From 00bcd070b7e1ee5ea89a5518a06d6f75e57bd20c Mon Sep 17 00:00:00 2001 From: "Ilya.Kazakevich" Date: Fri, 31 Mar 2017 02:54:43 +0300 Subject: [PATCH] PY-23279, PY-9963: Use CommandLine language to parse arguments passed as additional * GeneralCommandLine does not support "as is" command line: it should be splitted to arguments (due to ProcessBuilder interface). --- .../psi/impl/PyElementGeneratorImpl.java | 3 ++ .../universalTests/PyTestRunnerUtils.kt | 51 +++++++++++++++++++ .../universalTests/PyUniversalTests.kt | 2 +- .../test_with_markers/test_with_markers.py | 11 ++++ .../python/testing/PythonPyTestingTest.java | 41 +++++++++++++-- .../universalTests/PyTestRunnerUtilsKtTest.kt | 35 +++++++++++++ 6 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 python/testData/testRunner/env/pytest/test_with_markers/test_with_markers.py create mode 100644 python/testSrc/com/jetbrains/python/testing/universalTests/PyTestRunnerUtilsKtTest.kt diff --git a/python/src/com/jetbrains/python/psi/impl/PyElementGeneratorImpl.java b/python/src/com/jetbrains/python/psi/impl/PyElementGeneratorImpl.java index 3e867147cf1b..29225aba8798 100644 --- a/python/src/com/jetbrains/python/psi/impl/PyElementGeneratorImpl.java +++ b/python/src/com/jetbrains/python/psi/impl/PyElementGeneratorImpl.java @@ -71,6 +71,9 @@ public class PyElementGeneratorImpl extends PyElementGenerator { return createDummyFile(langLevel, contents, false); } + /** + * TODO: Use {@link PsiFileFactory} instead? + */ public PsiFile createDummyFile(LanguageLevel langLevel, String contents, boolean physical) { final PsiFileFactory factory = PsiFileFactory.getInstance(myProject); final String name = getDummyFileName(); diff --git a/python/src/com/jetbrains/python/testing/universalTests/PyTestRunnerUtils.kt b/python/src/com/jetbrains/python/testing/universalTests/PyTestRunnerUtils.kt index 884554da99f0..4a69aceaeda3 100644 --- a/python/src/com/jetbrains/python/testing/universalTests/PyTestRunnerUtils.kt +++ b/python/src/com/jetbrains/python/testing/universalTests/PyTestRunnerUtils.kt @@ -15,14 +15,22 @@ */ package com.jetbrains.python.testing.universalTests +import com.intellij.execution.ExecutionException import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.module.ModuleUtil import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiErrorElement +import com.intellij.psi.PsiFileFactory import com.intellij.psi.PsiFileSystemItem import com.intellij.psi.PsiManager import com.intellij.psi.util.QualifiedName +import com.jetbrains.commandInterface.commandLine.CommandLineLanguage +import com.jetbrains.commandInterface.commandLine.CommandLinePart +import com.jetbrains.commandInterface.commandLine.psi.CommandLineArgument +import com.jetbrains.commandInterface.commandLine.psi.CommandLineFile +import com.jetbrains.commandInterface.commandLine.psi.CommandLineOption import com.jetbrains.extensions.getQName import com.jetbrains.python.PyNames import com.jetbrains.python.psi.PyFile @@ -30,6 +38,7 @@ import com.jetbrains.python.psi.PyQualifiedNameOwner import com.jetbrains.python.psi.PyUtil import com.jetbrains.python.psi.resolve.fromModule import com.jetbrains.python.psi.resolve.resolveModuleAt +import java.util.* /** * @author Ilya.Kazakevich @@ -106,3 +115,45 @@ private fun findVFSItemRoot(virtualFile: VirtualFile, project: Project): Virtual } + +/** + * Emulates command line processor by parsing command line to arguments that can be provided as argv. + * Escape chars are not supported but quotes work. + * @throws ExecutionException if can't be parsed + */ +fun getParsedAdditionalArguments(project: Project, additionalArguments: String): List { + val factory = PsiFileFactory.getInstance(project) + val file = factory.createFileFromText(CommandLineLanguage.INSTANCE, + String.format("fake_command %s", additionalArguments)) as CommandLineFile + + if (file.children.any { it is PsiErrorElement }) { + throw ExecutionException("Additional arguments can't be parsed. Please check they are valid: $additionalArguments") + } + + + val additionalArgsList = ArrayList() + var skipArgument = false + file.children.filterIsInstance(CommandLinePart::class.java).forEach { + when (it) { + is CommandLineOption -> { + val optionText = it.text + val possibleArgument = it.findArgument() + if (possibleArgument != null) { + additionalArgsList.add(optionText + possibleArgument.valueNoQuotes) + skipArgument = true + } + else { + additionalArgsList.add(optionText) + } + } + is CommandLineArgument -> { + if (!skipArgument) { + additionalArgsList.add(it.valueNoQuotes) + } + skipArgument = false + } + } + } + return additionalArgsList +} + diff --git a/python/src/com/jetbrains/python/testing/universalTests/PyUniversalTests.kt b/python/src/com/jetbrains/python/testing/universalTests/PyUniversalTests.kt index 3a7bf25f66d2..ae0f70cb53f8 100644 --- a/python/src/com/jetbrains/python/testing/universalTests/PyUniversalTests.kt +++ b/python/src/com/jetbrains/python/testing/universalTests/PyUniversalTests.kt @@ -385,7 +385,7 @@ abstract class PyUniversalTestConfiguration(project: Project, private fun generateRawArguments(): List { val rawArguments = additionalArguments + " " + getCustomRawArgumentsString() if (rawArguments.isNotBlank()) { - return listOf("--") + rawArguments.trim().split(" ") + return listOf("--") + getParsedAdditionalArguments(project, additionalArguments) } return emptyList() } diff --git a/python/testData/testRunner/env/pytest/test_with_markers/test_with_markers.py b/python/testData/testRunner/env/pytest/test_with_markers/test_with_markers.py new file mode 100644 index 000000000000..a7b9646bbd78 --- /dev/null +++ b/python/testData/testRunner/env/pytest/test_with_markers/test_with_markers.py @@ -0,0 +1,11 @@ + + +import pytest + +@pytest.mark.slow +def test_slow(): + pass + + +def test_fast(): + pass \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/env/python/testing/PythonPyTestingTest.java b/python/testSrc/com/jetbrains/env/python/testing/PythonPyTestingTest.java index afc0870db292..a7cb9f2715cd 100644 --- a/python/testSrc/com/jetbrains/env/python/testing/PythonPyTestingTest.java +++ b/python/testSrc/com/jetbrains/env/python/testing/PythonPyTestingTest.java @@ -31,6 +31,39 @@ import static org.junit.Assert.assertEquals; @EnvTestTagsRequired(tags = "pytest") public final class PythonPyTestingTest extends PyEnvTestCase { + // Ensure slow test is not run when -m "not slow" is provided + @Test + public void testMarkerWithSpaces() throws Exception { + runPythonTest( + new PyProcessWithConsoleTestTask("/testRunner/env/pytest/test_with_markers", SdkCreationType.EMPTY_SDK) { + + @NotNull + @Override + protected PyTestTestProcessRunner createProcessRunner() throws Exception { + return new PyTestTestProcessRunner("test_with_markers.py", 0) { + @Override + protected void configurationCreatedAndWillLaunch(@NotNull PyUniversalPyTestConfiguration configuration) throws IOException { + super.configurationCreatedAndWillLaunch(configuration); + configuration.setAdditionalArguments("-m 'not slow'"); + } + }; + } + + + @Override + protected void checkTestResults(@NotNull PyTestTestProcessRunner runner, + @NotNull String stdout, + @NotNull String stderr, + @NotNull String all) { + Assert.assertEquals("Marker support broken", "Test tree:\n" + + "[root]\n" + + ".test_with_markers\n" + + "..test_fast(+)\n", + runner.getFormattedTestTree()); + } + }); + } + @Test public void testConfigurationProducer() throws Exception { runPythonTest( @@ -49,7 +82,8 @@ public final class PythonPyTestingTest extends PyEnvTestCase { @Test public void testTestsInSubFolderResolvable() throws Exception { runPythonTest( - new PyUnitTestProcessWithConsoleTestTask.PyTestsInSubFolderRunner("test_metheggs", "test_funeggs", "test_first") { + new PyUnitTestProcessWithConsoleTestTask.PyTestsInSubFolderRunner("test_metheggs", "test_funeggs", + "test_first") { @NotNull @Override protected PyTestTestProcessRunner createProcessRunner() throws Exception { @@ -110,7 +144,8 @@ public final class PythonPyTestingTest extends PyEnvTestCase { @Test public void testProduceConfigurationOnFile() throws Exception { runPythonTest( - new CreateConfigurationByFileTask(PythonTestConfigurationsModel.PY_TEST_NAME, PyUniversalPyTestConfiguration.class, "spam.py") { + new CreateConfigurationByFileTask(PythonTestConfigurationsModel.PY_TEST_NAME, + PyUniversalPyTestConfiguration.class, "spam.py") { @NotNull @Override protected PsiElement getElementToRightClickOnByFile(@NotNull final String fileName) { @@ -246,7 +281,7 @@ public final class PythonPyTestingTest extends PyEnvTestCase { if (getLevelForSdk().isPy3K()) { return new PyTestTestProcessRunner("folder_no_init_py/test_test.py", 2); } - else { + else { return new PyTestTestProcessRunner(toFullPath("folder_no_init_py/test_test.py"), 2) { @Override protected void configurationCreatedAndWillLaunch(@NotNull PyUniversalPyTestConfiguration configuration) throws IOException { diff --git a/python/testSrc/com/jetbrains/python/testing/universalTests/PyTestRunnerUtilsKtTest.kt b/python/testSrc/com/jetbrains/python/testing/universalTests/PyTestRunnerUtilsKtTest.kt new file mode 100644 index 000000000000..d6fc8a1c6ba1 --- /dev/null +++ b/python/testSrc/com/jetbrains/python/testing/universalTests/PyTestRunnerUtilsKtTest.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2000-2017 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.jetbrains.python.testing.universalTests + +import com.jetbrains.python.fixtures.PyTestCase +import org.junit.Assert +import org.junit.Test + +/** + * @author Ilya.Kazakevich + */ +class PyTestRunnerUtilsKtTest : PyTestCase() { + @Test + fun testGetParsedAdditionalArguments() { + var list = getParsedAdditionalArguments(myFixture.project, "-v --color=red -m 'spam and eggs'") + Assert.assertEquals("List parsed incorrectly", listOf("-v", "--color=red", "-m", "spam and eggs"), list) + + list = getParsedAdditionalArguments(myFixture.project, "--eggs=spam --foo=\"eggs and spam\"") + Assert.assertEquals("List parsed incorrectly", listOf("--eggs=spam", "--foo=eggs and spam"), list) + + } +} \ No newline at end of file