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 dd4049de6971..50cdf6c3dfb1 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 @@ -9,18 +9,35 @@ import com.jetbrains.python.psi.PyClass import com.jetbrains.python.psi.PyFile import com.jetbrains.python.psi.PyFunction import com.jetbrains.python.psi.types.TypeEvalContext +import com.jetbrains.python.testing.pyTestFixtures.TEST_FIXTURE_DECORATOR_NAMES /** - * Any "test_" function could be used as test by pytest. + * Pytest could use any "test_" function as a test. * See `PythonUnitTestDetectorsBasedOnSettings.isTestFunction`. */ -fun isTestFunction(function: PyFunction) = function.name?.startsWith("test") == true +fun isTestFunction(function: PyFunction): Boolean { + // Check if the function name starts with "test" + val isTestNamed = function.name?.startsWith("test") == true + if (!isTestNamed) return false + + // Get the list of decorators, if any, otherwise consider this a test function + val decoratorList = function.decoratorList?.decorators ?: return true + + // Determine if any decorator matches known pytest fixture names + val isPytestFixture = decoratorList.any { decorator -> + val decoratorName = decorator.callee?.text + decoratorName in TEST_FIXTURE_DECORATOR_NAMES + } + + // A test function must not be a pytest fixture + return !isPytestFixture +} /** * Inheritor of TestCase class is always test for unittest and could also be launched with pytest. * See `PythonUnitTestDetectorsBasedOnSettings.isTestFunction`. */ -fun isUnitTestCaseClass(clazz: PyClass, context: TypeEvalContext) = clazz.inherits(context, "unittest.TestCase", "unittest.case.TestCase") +fun isUnitTestCaseClass(clazz: PyClass, context: TypeEvalContext): Boolean = clazz.inherits(context, "unittest.TestCase", "unittest.case.TestCase") /** * Checks if class [isUnitTestCaseClass] or both conditions are true: it's name starts/ends with "Test" and it has at least one @@ -55,13 +72,13 @@ fun isTestClass(clazz: PyClass, context: TypeEvalContext): Boolean { fun isTestFile(file: PyFile, context: TypeEvalContext): Boolean { - return if (file.topLevelClasses.stream().anyMatch { o: PyClass -> + return if (file.topLevelClasses.any { o: PyClass -> isTestClass(o, context) }) { true } else file.name.startsWith("test_") || - file.topLevelFunctions.stream().anyMatch { o: PyFunction -> + file.topLevelFunctions.any { o: PyFunction -> isTestFunction(o) } } diff --git a/python/testData/pyTestLineMarker/fixtureTest.py b/python/testData/pyTestLineMarker/fixtureTest.py new file mode 100644 index 000000000000..dfd484336734 --- /dev/null +++ b/python/testData/pyTestLineMarker/fixtureTest.py @@ -0,0 +1,8 @@ +import pytest + +@pytest.fixture +def test_fixture(): + return 42 + +def test_actual(): + assert True \ 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 b3f2e636b5d8..65ff332e55e9 100644 --- a/python/testSrc/com/jetbrains/python/testing/PyTestRunLineMarkerTest.kt +++ b/python/testSrc/com/jetbrains/python/testing/PyTestRunLineMarkerTest.kt @@ -6,12 +6,12 @@ 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" + const val FIXTURE_FILE = "fixtureTest.py" } override fun getTestDataPath(): String = super.getTestDataPath() + TESTS_DIR @@ -25,9 +25,9 @@ open class PyTestRunLineMarkerTest : PyTestCase() { protected fun getCaretElement(fileName: String): PsiElement? { val psiFile = configureByFile(fileName) - TestCase.assertNotNull("Can't find test file", psiFile) + assertNotNull("Can't find test file", psiFile) val element = psiFile?.findElementAt(myFixture.caretOffset) - TestCase.assertNotNull("Can't find caret element", element) + assertNotNull("Can't find caret element", element) return element } @@ -37,14 +37,14 @@ open class PyTestRunLineMarkerTest : PyTestCase() { protected fun assertInfoFound(element: PsiElement, lineMarkerContributor: RunLineMarkerContributor) { val info = getInfo(element, lineMarkerContributor) - TestCase.assertNotNull("Info is not found", info) + assertNotNull("Info is not found", info) if (info != null) { - TestCase.assertNotNull("Run icon is not found", info.icon) + assertNotNull("Run icon is not found", info.icon) } } protected fun assertInfoNotFound(element: PsiElement, lineMarkerContributor: RunLineMarkerContributor) { - TestCase.assertNull("Info is found", getInfo(element, lineMarkerContributor)) + assertNull("Info is found", getInfo(element, lineMarkerContributor)) } fun testPythonFile() { @@ -54,4 +54,23 @@ open class PyTestRunLineMarkerTest : PyTestCase() { assertInfoFound(element, lineMarkerContributor) } } + + fun testFixtureWithTestPrefix() { + val lineMarkerContributor = PyTestLineMarkerContributor() + + myFixture.configureByText(FIXTURE_FILE, """ + import pytest + + @pytest.fixture + def test_fixture(): + return 42 + + def test_actual(): + assert True + """.trimIndent()) + + val fixtureElement = myFixture.file.findElementAt(myFixture.caretOffset) + assertNotNull(fixtureElement) + assertInfoNotFound(fixtureElement!!, lineMarkerContributor) + } } \ No newline at end of file