From 4508e0de9d260483e5f4a59931fcb00a6097fb91 Mon Sep 17 00:00:00 2001 From: "Ilya.Kazakevich" Date: Tue, 10 Dec 2019 22:57:57 +0300 Subject: [PATCH] PY-31836: Use pytest parameter as test name, not keyword. The correct way to use parameters is to add them to the test name. "-k" is also works in some cases, but it is not official nor stable. * pytest runner reports "testname[param]" as metainfo * we fetch param * add "parameters" field to the pytest * glue it with test name GitOrigin-RevId: 56c5d93fd93a12ba5742b21af51ea04823a9d8f7 --- .../helpers/pycharm/teamcity/pytest_plugin.py | 5 +-- .../com/jetbrains/python/testing/PyTest.kt | 34 ++++++++++++-- .../jetbrains/python/testing/PyTestsShared.kt | 19 ++++---- .../python/testing/PythonPyTestingTest.java | 44 +++++++++++++++++-- 4 files changed, 83 insertions(+), 19 deletions(-) diff --git a/python/helpers/pycharm/teamcity/pytest_plugin.py b/python/helpers/pycharm/teamcity/pytest_plugin.py index a201cf2f3ab2..336e40ef31b7 100644 --- a/python/helpers/pycharm/teamcity/pytest_plugin.py +++ b/python/helpers/pycharm/teamcity/pytest_plugin.py @@ -213,9 +213,8 @@ class EchoTeamCityMessages(object): def pytest_runtest_logstart(self, nodeid, location): # test name fetched from location passed as metainfo to PyCharm - # it will be used to run specific test using "-k" - # See IDEA-176950 - # We only need method/function name because only it could be used as -k + # it will be used to run specific test + # See IDEA-176950, PY-31836 test_name = location[2] if test_name: test_name = str(test_name).split(".")[-1] diff --git a/python/src/com/jetbrains/python/testing/PyTest.kt b/python/src/com/jetbrains/python/testing/PyTest.kt index 4180c6198868..cf6d735de612 100644 --- a/python/src/com/jetbrains/python/testing/PyTest.kt +++ b/python/src/com/jetbrains/python/testing/PyTest.kt @@ -27,15 +27,22 @@ import com.intellij.psi.search.GlobalSearchScope import com.jetbrains.python.PyNames import com.jetbrains.python.PythonHelper import com.jetbrains.python.run.targetBasedConfiguration.PyRunTargetVariant +import com.jetbrains.python.testing.PyTestSharedForm.* /** * Pytest runner */ +//Fetch param from parametrized test name spam[eggs] +private val PARAM_REGEX = Regex("\\[(.+)]$") + class PyTestSettingsEditor(configuration: PyAbstractTestConfiguration) : PyAbstractTestSettingsEditor( - PyTestSharedForm.create(configuration, PyTestSharedForm.CustomOption( - PyTestConfiguration::keywords.name, PyRunTargetVariant.PATH, PyRunTargetVariant.PYTHON))) + create( + configuration, + CustomOption(PyTestConfiguration::keywords.name, PyRunTargetVariant.PATH, PyRunTargetVariant.PYTHON), + CustomOption(PyTestConfiguration::parameters.name, PyRunTargetVariant.PATH, PyRunTargetVariant.PYTHON) + )) class PyPyTestExecutionEnvironment(configuration: PyTestConfiguration, environment: ExecutionEnvironment) : PyTestExecutionEnvironment(configuration, environment) { @@ -52,6 +59,8 @@ class PyTestConfiguration(project: Project, factory: PyTestFactory) : PyAbstractTestConfiguration(project, factory, PyTestFrameworkService.getSdkReadableNameByFramework(PyNames.PY_TEST)) { @ConfigField var keywords: String = "" + @ConfigField + var parameters: String = "" override fun getState(executor: Executor, environment: ExecutionEnvironment): RunProfileState? = PyPyTestExecutionEnvironment(this, environment) @@ -70,14 +79,31 @@ class PyTestConfiguration(project: Project, factory: PyTestFactory) return target.generateArgumentsLine(this) + listOf(rawArgumentsSeparator, "--last-failed") } + override fun getTestSpec(): List { + // Parametrized test must add parameter to target. + // So, foo.spam becomes foo.spam[param] + if (parameters.isNotEmpty() && target.targetType == PyRunTargetVariant.PYTHON) { + return super.getTestSpec().toMutableList().apply { + this[size - 1] = last() + "[$parameters]" + } + } + return super.getTestSpec() + } + override fun isFrameworkInstalled(): Boolean = VFSTestFrameworkListener.getInstance().isTestFrameworkInstalled(sdk, PyNames.PY_TEST) override fun setMetaInfo(metaInfo: String) { - keywords = metaInfo + // Metainfo contains test name along with params. + parameters = getParamFromMetaInfo(metaInfo) } + /** + * Fetch params from test name + */ + private fun getParamFromMetaInfo(metaInfo: String) = PARAM_REGEX.find(metaInfo)?.groupValues?.getOrNull(1) ?: "" + override fun isSameAsLocation(target: ConfigurationTarget, metainfo: String?): Boolean { - return super.isSameAsLocation(target, metainfo) && metainfo == keywords + return super.isSameAsLocation(target, metainfo) && getParamFromMetaInfo(metainfo ?: "") == parameters } } diff --git a/python/src/com/jetbrains/python/testing/PyTestsShared.kt b/python/src/com/jetbrains/python/testing/PyTestsShared.kt index 66ccb5defbbc..02814a7b4a01 100644 --- a/python/src/com/jetbrains/python/testing/PyTestsShared.kt +++ b/python/src/com/jetbrains/python/testing/PyTestsShared.kt @@ -63,10 +63,10 @@ import java.util.regex.Matcher * New configuration factories */ internal val pythonFactories get() = arrayOf( - PyUnitTestFactory(), - PyTestFactory(), - PyNoseTestFactory(), - PyTrialTestFactory()) + PyUnitTestFactory(), + PyTestFactory(), + PyNoseTestFactory(), + PyTrialTestFactory()) /** * Accepts text that may be wrapped in TC message. Unwraps it and removes TC escape code. @@ -110,7 +110,7 @@ fun isTestElement(element: PsiElement, testCaseClassRequired: ThreeState, typeEv /** * Since runners report names of tests as qualified name, no need to convert it to PSI and back to string. * We just save its name and provide it again to rerun - * @param metainfo additional info provided by test runner, in case of pytest it is test name that could be used as "-k" argument + * @param metainfo additional info provided by test runner, in case of pytest it is test name with parameters (if test is parametrized) */ private class PyTargetBasedPsiLocation(val target: ConfigurationTarget, element: PsiElement, @@ -483,8 +483,8 @@ abstract class PyAbstractTestConfiguration(project: Project, return ConfigurationTarget(qualifiedName, PyRunTargetVariant.PYTHON).generateArgumentsLine(this) } - override fun getTestSpec(location: Location<*>, - failedTest: AbstractTestProxy): String? { + final override fun getTestSpec(location: Location<*>, + failedTest: AbstractTestProxy): String? { val list = getPythonTestSpecByLocation(location) if (list.isEmpty()) { return null @@ -504,7 +504,7 @@ abstract class PyAbstractTestConfiguration(project: Project, return result + generateRawArguments(true) } - fun getTestSpec(): List { + open fun getTestSpec(): List { return target.generateArgumentsLine(this) + generateRawArguments() } @@ -613,7 +613,8 @@ abstract class PyAbstractTestConfiguration(project: Project, internal open fun shouldSeparateTargetPath(): Boolean = true /** - * @param metaInfo String "metainfo" field provided by test runner + * @param metaInfo String "metainfo" field provided by test runner. + * Pytest reports test name with parameters here */ open fun setMetaInfo(metaInfo: String) { diff --git a/python/testSrc/com/jetbrains/env/python/testing/PythonPyTestingTest.java b/python/testSrc/com/jetbrains/env/python/testing/PythonPyTestingTest.java index 24a7437e00ab..5f47bce7e77c 100644 --- a/python/testSrc/com/jetbrains/env/python/testing/PythonPyTestingTest.java +++ b/python/testSrc/com/jetbrains/env/python/testing/PythonPyTestingTest.java @@ -178,7 +178,7 @@ public final class PythonPyTestingTest extends PyEnvTestCase { } /** - * Test name must be reported as meta info to be used as argument for "-k" for parametrized tests + * Test name must be reported as meta info to be used as parameter for for parametrized tests */ @Test public void testMetaInfoForMethod() { @@ -234,6 +234,44 @@ public final class PythonPyTestingTest extends PyEnvTestCase { }); } + /** + * Ensure that testName[param] is only launched for parametrized test if param provided + */ + @Test + public void testParametrizedRunByParameter() { + runPythonTest( + new PyProcessWithConsoleTestTask("/testRunner/env/pytest/parametrized", SdkCreationType.EMPTY_SDK) { + + @NotNull + @Override + protected PyTestTestProcessRunner createProcessRunner() { + return new PyTestTestProcessRunner("test_pytest_parametrized.py", 1) { + @Override + protected void configurationCreatedAndWillLaunch(@NotNull PyTestConfiguration configuration) throws IOException { + super.configurationCreatedAndWillLaunch(configuration); + configuration.getTarget().setTarget("test_pytest_parametrized.test_eval"); + configuration.getTarget().setTargetType(PyRunTargetVariant.PYTHON); + configuration.setMetaInfo("test_eval[three plus file-8]"); + } + }; + } + + @Override + protected void checkTestResults(@NotNull final PyTestTestProcessRunner runner, + @NotNull final String stdout, + @NotNull final String stderr, + @NotNull final String all, int exitCode) { + assertEquals("Only one test should be launched", + "Test tree:\n" + + "[root](-)\n" + + ".test_pytest_parametrized(-)\n" + + "..test_eval(-)\n" + + "...(three plus file-8)(-)\n", + runner.getFormattedTestTree()); + } + }); + } + /** * See https://github.com/JetBrains/teamcity-messages/issues/131 @@ -497,8 +535,8 @@ public final class PythonPyTestingTest extends PyEnvTestCase { configuration.setWorkingDirectory(myFixture.getTempDirPath()); ReadAction.run(() -> Assert.assertThat("Failed to resolve qname", - configuration.getTarget().asPsiElement(configuration), - Matchers.instanceOf(PyFile.class))); + configuration.getTarget().asPsiElement(configuration), + Matchers.instanceOf(PyFile.class))); } }); }