mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-13 09:19:13 +07:00
PY-34498 Add an inspection for pytest fixture that is not passed to test parameters
Report warning if a fixture is used without being passed to test function parameters or to `@pytest.mark.usefixtures` decorator. Co-authored-by: Denis Mashutin <Denis.Mashutin@jetbrains.com> Merge-request: IJ-MR-108713 Merged-by: Egor Eliseev <Egor.Eliseev@jetbrains.com> GitOrigin-RevId: 28d0711b99ab7ae180f672306dd4ab8a81f1feec
This commit is contained in:
committed by
intellij-monorepo-bot
co-authored by
Denis Mashutin
parent
42557af56d
commit
540f24faa4
@@ -0,0 +1,5 @@
|
||||
<html>
|
||||
<body>
|
||||
<p>Reports if a fixture is used without being passed to test function parameters or to <code>@pytest.mark.usefixtures</code> decorator</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -546,6 +546,11 @@ QFIX.NAME.install.and.import.package=Install and import package ''{0}''
|
||||
# PyAsyncCallInspection
|
||||
QFIX.coroutine.is.not.awaited=Coroutine is not awaited
|
||||
|
||||
# PyTestUnpassedFixtureInspection
|
||||
INSP.NAME.pytest.unpassed.fixture=Fixture is not requested by test functions
|
||||
INSP.use.fixture.without.declaration.in.test.function=Fixture ''{0}'' is not requested by test functions or '@pytest.mark.usefixtures' marker
|
||||
QFIX.add.fixture.to.test.function.parameters.list=Add fixture to test function parameters
|
||||
|
||||
|
||||
# Actions and associated commands
|
||||
ACT.CMD.use.import=Use an imported module
|
||||
|
||||
@@ -180,6 +180,10 @@
|
||||
bundle="messages.PyPsiBundle" key="INSP.NAME.pytest-parametrized" groupKey="INSP.GROUP.python"
|
||||
enabledByDefault="true" level="WARNING"
|
||||
implementationClass="com.jetbrains.python.testing.pyTestParametrized.PyTestParametrizedInspection"/>
|
||||
<localInspection language="Python" shortName="PyTestUnpassedFixtureInspection" suppressId="PyTestUnpassedFixture"
|
||||
bundle="messages.PyPsiBundle" key="INSP.NAME.pytest.unpassed.fixture" groupKey="INSP.GROUP.python"
|
||||
enabledByDefault="true" level="WARNING"
|
||||
implementationClass="com.jetbrains.python.testing.pyTestFixtures.PyTestUnpassedFixtureInspection"/>
|
||||
<localInspection shortName="PyPackageRequirementsInspection" suppressId="PyPackageRequirements" bundle="messages.PyPsiBundle"
|
||||
key="INSP.NAME.requirements" groupKey="INSP.GROUP.python" enabledByDefault="true" level="WARNING"
|
||||
implementationClass="com.jetbrains.python.inspections.PyPackageRequirementsInspection"/>
|
||||
|
||||
@@ -30,22 +30,32 @@ private fun PyDecoratorList.hasDecorator(vararg names: String) = names.any { fin
|
||||
|
||||
private fun PyElement.getFixtureName() = name ?: (this as? PyStringLiteralExpression)?.stringValue
|
||||
|
||||
/**
|
||||
* Needed to avoid using the too wide `PyExpression` type
|
||||
*/
|
||||
private sealed class PyParameterOrPyReferenceExpression(val expression: PyExpression) {
|
||||
class Parameter(expression: PyExpression) : PyParameterOrPyReferenceExpression(expression)
|
||||
class Reference(expression: PyExpression) : PyParameterOrPyReferenceExpression(expression)
|
||||
}
|
||||
|
||||
internal fun getFixtureLink(element: PyElement, typeEvalContext: TypeEvalContext): NamedFixtureLink? {
|
||||
val module = ModuleUtilCore.findModuleForPsiElement(element) ?: return null
|
||||
return when (element) {
|
||||
is PyNamedParameter -> getFixtureAsParameterLink(element, typeEvalContext, module)
|
||||
is PyNamedParameter -> getFixtureAsExpressionLink(PyParameterOrPyReferenceExpression.Parameter(element), typeEvalContext, module)
|
||||
is PyReferenceExpression -> getFixtureAsExpressionLink(PyParameterOrPyReferenceExpression.Reference(element), typeEvalContext, module)
|
||||
is PyStringLiteralExpression -> getFixtureAsStringLink(element, typeEvalContext, module)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If named parameter has fixture (and import statement) -- return it
|
||||
* If named parameter or reference expression has fixture (and import statement) -- return it
|
||||
*/
|
||||
private fun getFixtureAsParameterLink(element: PyNamedParameter, typeEvalContext: TypeEvalContext, module: Module): NamedFixtureLink? {
|
||||
val func = PsiTreeUtil.getParentOfType(element, PyFunction::class.java) ?: return null
|
||||
val fixtureCandidates = getFixtures(module, func, typeEvalContext).filter { o -> o.name == element.name }
|
||||
return module.basePath?.let { return findRightFixture(fixtureCandidates, func, element, typeEvalContext, it) }
|
||||
private fun getFixtureAsExpressionLink(element: PyParameterOrPyReferenceExpression, typeEvalContext: TypeEvalContext, module: Module): NamedFixtureLink? {
|
||||
val pyExpression = element.expression
|
||||
val func = PsiTreeUtil.getParentOfType(pyExpression, PyFunction::class.java) ?: return null
|
||||
val fixtureCandidates = getFixtures(module, func, typeEvalContext).filter { o -> o.name == pyExpression.name }
|
||||
return module.basePath?.let { return findRightFixture(fixtureCandidates, func, pyExpression, typeEvalContext, it) }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,7 +113,8 @@ private fun findRightFixture(fixtureCandidates: List<PyTestFixture>,
|
||||
if (!fixtureCandidates.isEmpty()) {
|
||||
val containingClass = if (pyFixtureElement is PyStringLiteralExpression) {
|
||||
PsiTreeUtil.getParentOfType<PyDecorator>(pyFixtureElement)?.target
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
func
|
||||
}?.containingClass
|
||||
containingClass?.let { pyClass ->
|
||||
@@ -143,7 +154,7 @@ private fun findRightFixture(fixtureCandidates: List<PyTestFixture>,
|
||||
|
||||
// search reserved fixture class in "_pytest" dir
|
||||
if (elementName in reservedFixtureClassSet) {
|
||||
return NamedFixtureLink(PyTestFixture(null, null, elementName), null)
|
||||
return NamedFixtureLink(PyTestFixture(null, null, elementName), null)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
+8
-1
@@ -2,17 +2,24 @@
|
||||
package com.jetbrains.python.testing.pyTestFixtures
|
||||
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiReference
|
||||
import com.jetbrains.python.inspections.PyInspectionExtension
|
||||
import com.jetbrains.python.psi.PyElement
|
||||
import com.jetbrains.python.psi.PyFunction
|
||||
import com.jetbrains.python.psi.PyNamedParameter
|
||||
import com.jetbrains.python.psi.types.TypeEvalContext
|
||||
|
||||
/**
|
||||
* fixture-based parameters should be skipped by inspection
|
||||
* Fixture-based parameters should be skipped by inspection.
|
||||
* Fixture-based unresolved reference should be ignored by inspection.
|
||||
* @see PyTestUnpassedFixtureInspection
|
||||
*/
|
||||
class PyTestFixtureInspectionExtension : PyInspectionExtension() {
|
||||
override fun ignoreUnused(local: PsiElement, evalContext: TypeEvalContext) =
|
||||
local is PyNamedParameter && local.isFixture(evalContext)
|
||||
|
||||
override fun ignoreShadowed(element: PsiElement) = element is PyFunction && element.isFixture()
|
||||
|
||||
override fun ignoreUnresolvedReference(node: PyElement, reference: PsiReference, context: TypeEvalContext) =
|
||||
getFixtureLink(node, context) != null
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.testing.pyTestFixtures
|
||||
|
||||
import com.intellij.codeInspection.*
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.psi.PsiElementVisitor
|
||||
import com.intellij.psi.util.findParentOfType
|
||||
import com.jetbrains.python.PyPsiBundle
|
||||
import com.jetbrains.python.inspections.PyInspection
|
||||
import com.jetbrains.python.inspections.PyInspectionVisitor
|
||||
import com.jetbrains.python.psi.PyDecorator
|
||||
import com.jetbrains.python.psi.PyElementGenerator
|
||||
import com.jetbrains.python.psi.PyFunction
|
||||
import com.jetbrains.python.psi.PyReferenceExpression
|
||||
import com.jetbrains.python.psi.PyStringLiteralExpression
|
||||
import com.jetbrains.python.psi.types.PyFunctionType
|
||||
import com.jetbrains.python.psi.types.TypeEvalContext
|
||||
|
||||
|
||||
/**
|
||||
* Reports if a fixture is used without being passed to test function parameters.
|
||||
* Suggests a quick fix "Add fixture to test function parameters"
|
||||
*/
|
||||
class PyTestUnpassedFixtureInspection : PyInspection() {
|
||||
|
||||
/**
|
||||
* Describes an `element` type into which `element: PyReferenceExpression` resolves.
|
||||
*
|
||||
* Type is `PyFixture` -> `FIXTURE`
|
||||
*
|
||||
* Type is unresolved -> `NONE`
|
||||
*
|
||||
* else -> `OTHER`
|
||||
*/
|
||||
private enum class ResolveType {
|
||||
FIXTURE,
|
||||
NONE,
|
||||
OTHER
|
||||
}
|
||||
|
||||
private val quickFix = AddFixtureToFuncParametersQuickFix()
|
||||
|
||||
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor {
|
||||
return Visitor(holder, PyInspectionVisitor.getContext(session))
|
||||
}
|
||||
|
||||
private fun findTestFunc(element: PyReferenceExpression) = element.findParentOfType<PyFunction>()
|
||||
|
||||
private fun isDecorator(element: PyReferenceExpression) = element.findParentOfType<PyDecorator>() != null
|
||||
|
||||
private inner class Visitor(holder: ProblemsHolder, context: TypeEvalContext) : PyInspectionVisitor(holder, context) {
|
||||
|
||||
override fun visitPyReferenceExpression(element: PyReferenceExpression) {
|
||||
if (isDecorator(element)) return
|
||||
|
||||
val testFunction = findTestFunc(element) ?: return
|
||||
// no warning if a fixture is in test function parameters
|
||||
testFunction.parameterList.parameters.find { it.name == element.name }?.let { return }
|
||||
|
||||
// no warning if a fixture is in '@pytest.mark.usefixtures' arguments
|
||||
if (isParameterInDecorator(element, testFunction)) return
|
||||
|
||||
// no warning if an element has type OTHER
|
||||
if (getType(element) == ResolveType.OTHER) return
|
||||
|
||||
// try to find a fixture from conftest.py file
|
||||
getFixtureLink(element, myTypeEvalContext) ?: return
|
||||
|
||||
holder?.registerProblem(element, PyPsiBundle.message("INSP.use.fixture.without.declaration.in.test.function", element.text),
|
||||
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
|
||||
quickFix)
|
||||
}
|
||||
|
||||
private fun getType(element: PyReferenceExpression): ResolveType {
|
||||
val type = myTypeEvalContext.getType(element) ?: return ResolveType.NONE
|
||||
val fixture = (type as? PyFunctionType)?.callable
|
||||
if (fixture is PyFunction && fixture.isFixture()) {
|
||||
return ResolveType.FIXTURE
|
||||
}
|
||||
return ResolveType.OTHER
|
||||
}
|
||||
|
||||
private fun isParameterInDecorator(element: PyReferenceExpression, testFunction: PyFunction): Boolean {
|
||||
val pytestUseFixturesDecorator = testFunction.decoratorList?.decorators?.find { it.name == USE_FIXTURES }
|
||||
return pytestUseFixturesDecorator?.argumentList?.arguments?.find { it is PyStringLiteralExpression && it.stringValue == element.name } != null
|
||||
}
|
||||
}
|
||||
|
||||
private inner class AddFixtureToFuncParametersQuickFix : LocalQuickFix {
|
||||
override fun getFamilyName() = PyPsiBundle.message("QFIX.add.fixture.to.test.function.parameters.list")
|
||||
|
||||
override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
|
||||
val element = descriptor.psiElement as? PyReferenceExpression ?: return
|
||||
val newParameter = PyElementGenerator.getInstance(project).createParameter(element.text)
|
||||
findTestFunc(element)?.parameterList?.addParameter(newParameter)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def foo():
|
||||
return 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("foo")
|
||||
def test_():
|
||||
assert foo == 1
|
||||
@@ -0,0 +1,10 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def foo():
|
||||
return 1
|
||||
|
||||
|
||||
def test_(foo):
|
||||
assert foo == 1
|
||||
@@ -0,0 +1,7 @@
|
||||
import pytest
|
||||
|
||||
|
||||
foo = 1
|
||||
|
||||
def test_():
|
||||
assert foo == 1
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import pytest
|
||||
from ResolvedExpression import foo
|
||||
|
||||
|
||||
def test_():
|
||||
assert foo == 1
|
||||
@@ -0,0 +1,10 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def foo():
|
||||
return 1
|
||||
|
||||
|
||||
def test_():
|
||||
assert <warning descr="Fixture 'foo' is not requested by test functions or @pytest.mark.usefixtures marker">foo</warning> == 1
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import pytest
|
||||
|
||||
|
||||
def test_():
|
||||
assert <warning descr="Fixture 'bar' is not requested by test functions or @pytest.mark.usefixtures marker">bar</warning> == 1
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import pytest
|
||||
from FixtureInDecorator import foo
|
||||
|
||||
def test_():
|
||||
assert <warning descr="Fixture 'foo' is not requested by test functions or @pytest.mark.usefixtures marker">foo</warning> == 1
|
||||
@@ -0,0 +1,6 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bar():
|
||||
return 1
|
||||
@@ -0,0 +1,6 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bar():
|
||||
return 1
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def foo():
|
||||
return 1
|
||||
|
||||
|
||||
def test_():
|
||||
assert <warning descr="Fixture 'foo' is not requested by test functions or @pytest.mark.usefixtures marker"><caret>foo</warning> == 1
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def foo():
|
||||
return 1
|
||||
|
||||
|
||||
def test_(foo):
|
||||
assert foo == 1
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import pytest
|
||||
|
||||
|
||||
def test_():
|
||||
assert <warning descr="Fixture 'bar' is not requested by test functions or @pytest.mark.usefixtures marker"><caret>bar</warning> == 1
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import pytest
|
||||
|
||||
|
||||
def test_(bar):
|
||||
assert bar == 1
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import pytest
|
||||
from simpleUnpassedFixture import foo
|
||||
|
||||
def test_():
|
||||
assert <warning descr="Fixture 'foo' is not requested by test functions or @pytest.mark.usefixtures marker"><caret>foo</warning> == 1
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import pytest
|
||||
from simpleUnpassedFixture import foo
|
||||
|
||||
def test_(foo):
|
||||
assert foo == 1
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.jetbrains.python.quickFixes
|
||||
|
||||
import com.jetbrains.python.PyPsiBundle
|
||||
import com.jetbrains.python.PyQuickFixTestCase
|
||||
import com.jetbrains.python.psi.LanguageLevel
|
||||
import com.jetbrains.python.testing.PythonTestConfigurationType
|
||||
import com.jetbrains.python.testing.TestRunnerService
|
||||
import com.jetbrains.python.testing.pyTestFixtures.PyTestUnpassedFixtureInspection
|
||||
|
||||
class PyTestAddFixtureToFuncParameterQuickFixTest : PyQuickFixTestCase() {
|
||||
|
||||
private fun doTest() {
|
||||
doQuickFixTest(PyTestUnpassedFixtureInspection::class.java,
|
||||
PyPsiBundle.message("QFIX.add.fixture.to.test.function.parameters.list"),
|
||||
LanguageLevel.getLatest())
|
||||
}
|
||||
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
TestRunnerService.getInstance(myFixture.module).selectedFactory =
|
||||
PythonTestConfigurationType.getInstance().pyTestFactory
|
||||
myFixture.copyDirectoryToProject("", "")
|
||||
}
|
||||
|
||||
fun testSimpleUnpassedFixture() {
|
||||
doTest()
|
||||
}
|
||||
|
||||
fun testUnpassedFixtureFromImport() {
|
||||
doTest()
|
||||
}
|
||||
|
||||
fun testUnpassedFixtureFromConftest() {
|
||||
doTest()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright 2000-2023 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.testFramework.LightProjectDescriptor
|
||||
import com.jetbrains.python.fixtures.PyInspectionTestCase
|
||||
import com.jetbrains.python.inspections.PyInspection
|
||||
import com.jetbrains.python.testing.pyTestFixtures.PyTestUnpassedFixtureInspection
|
||||
|
||||
class PyTestUnpassedFixtureInspectionTest : PyInspectionTestCase() {
|
||||
override fun getInspectionClass(): Class<out PyInspection> {
|
||||
return PyTestUnpassedFixtureInspection::class.java
|
||||
}
|
||||
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
TestRunnerService.getInstance(myFixture.module).selectedFactory =
|
||||
PythonTestConfigurationType.getInstance().pyTestFactory
|
||||
myFixture.copyDirectoryToProject("", "")
|
||||
}
|
||||
|
||||
override fun getProjectDescriptor(): LightProjectDescriptor? {
|
||||
return ourPyLatestDescriptor
|
||||
}
|
||||
|
||||
override fun isLowerCaseTestFile(): Boolean = false
|
||||
|
||||
fun testFixtureInDecorator() {
|
||||
doTest()
|
||||
}
|
||||
|
||||
fun testFixtureInParameters() {
|
||||
doTest()
|
||||
}
|
||||
|
||||
fun testResolvedExpression() {
|
||||
doTest()
|
||||
}
|
||||
|
||||
fun testResolvedExpressionFromImport() {
|
||||
doTest()
|
||||
}
|
||||
|
||||
fun testSimpleUnpassedFixture() {
|
||||
doTest()
|
||||
}
|
||||
|
||||
fun testUnpassedFixtureFromConftest() {
|
||||
doTest()
|
||||
}
|
||||
|
||||
fun testUnpassedFixtureFromImport() {
|
||||
doTest()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user