diff --git a/python/helpers/pycharm/docrunner.py b/python/helpers/pycharm/docrunner.py index 2ccfcfdb29b1..82c958b229e9 100644 --- a/python/helpers/pycharm/docrunner.py +++ b/python/helpers/pycharm/docrunner.py @@ -1,6 +1,7 @@ import sys import datetime import os + helpers_dir = os.getenv("PYCHARM_HELPERS_DIR", sys.path[0]) if sys.path[0] != helpers_dir: sys.path.insert(0, helpers_dir) @@ -16,6 +17,32 @@ adjust_sys_path() re = import_system_module("re") doctest = import_system_module("doctest") traceback = import_system_module("traceback") +argparse = import_system_module("argparse") + +_OPTIONFLAGS_BY_NAME = {} + + +def _register_all_optionflags(): + """ + Needed for correct parsing docrunner.py arguments + See: https://github.com/python/cpython/blob/main/Lib/doctest.py + """ + def _register_optionflag(name): + # Create a new flag unless `name` is already known. + return _OPTIONFLAGS_BY_NAME.setdefault(name, 1 << len(_OPTIONFLAGS_BY_NAME)) + + _register_optionflag('DONT_ACCEPT_TRUE_FOR_1') + _register_optionflag('DONT_ACCEPT_BLANKLINE') + _register_optionflag('NORMALIZE_WHITESPACE') + _register_optionflag('ELLIPSIS') + _register_optionflag('SKIP') + _register_optionflag('IGNORE_EXCEPTION_DETAIL') + _register_optionflag('REPORT_UDIFF') + _register_optionflag('REPORT_CDIFF') + _register_optionflag('REPORT_NDIFF') + _register_optionflag('REPORT_ONLY_FIRST_FAILURE') + _register_optionflag('FAIL_FAST') + class TeamcityDocTestResult(TeamcityTestResult): """ @@ -199,10 +226,6 @@ class DocTestRunner(doctest.DocTestRunner): modules = {} - -runner = DocTestRunner() - - def _load_file(moduleName, fileName): if sys.version_info >= (3, 5): import importlib @@ -273,7 +296,34 @@ def testFilesInFolderUsingPattern(folder, pattern = ".*"): return modules +def _parse_args(): + _register_all_optionflags() + parser = argparse.ArgumentParser() + + parser.add_argument('-v', '--verbose', action='store_true', default=False) + parser.add_argument('-o', '--option', action='append', + choices=_OPTIONFLAGS_BY_NAME.keys(), default=[]) + parser.add_argument('-f', '--fail-fast', action='store_true') + + original_argv = sys.argv + sys.argv = original_argv[1:] + args = parser.parse_args() + sys.argv = original_argv + + verbose = args.verbose + + options = 0 + for option in args.option: + options |= _OPTIONFLAGS_BY_NAME[option] + if args.fail_fast: + options |= _OPTIONFLAGS_BY_NAME['FAIL_FAST'] + + return verbose, options + + if __name__ == "__main__": + verbose, options = _parse_args() + runner = DocTestRunner(verbose=verbose, optionflags=options) finder = doctest.DocTestFinder() for arg in sys.argv[1:]: @@ -281,6 +331,9 @@ if __name__ == "__main__": if len(arg) == 0: continue + if arg.startswith("-") or arg in _OPTIONFLAGS_BY_NAME.keys(): + continue + a = arg.split("::") if len(a) == 1: # From module or folder diff --git a/python/helpers/pydev/pydevd_tracing.py b/python/helpers/pydev/pydevd_tracing.py index 8e531c78b570..4af935a261f9 100644 --- a/python/helpers/pydev/pydevd_tracing.py +++ b/python/helpers/pydev/pydevd_tracing.py @@ -50,7 +50,7 @@ def _get_stack_str(frame): return msg def _internal_set_trace(tracing_func): - if TracingFunctionHolder._warn: + if TracingFunctionHolder._warn and sys.gettrace() != tracing_func: frame = get_frame() if frame is not None and frame.f_back is not None: filename = frame.f_back.f_code.co_filename.lower() diff --git a/python/src/com/jetbrains/python/testing/PythonTestLegacyRunConfigurationForm.java b/python/src/com/jetbrains/python/testing/PythonTestLegacyRunConfigurationForm.java index 55dc1cbb6801..0124eb2c7401 100644 --- a/python/src/com/jetbrains/python/testing/PythonTestLegacyRunConfigurationForm.java +++ b/python/src/com/jetbrains/python/testing/PythonTestLegacyRunConfigurationForm.java @@ -284,6 +284,10 @@ public class PythonTestLegacyRunConfigurationForm implements AbstractPythonTestR return myParamCheckBox; } + public JTextField getParamTextField() { + return myParamTextField; + } + public void setParams(String params) { myParamTextField.setText(params); } diff --git a/python/src/com/jetbrains/python/testing/doctest/PythonDocTestCommandLineState.kt b/python/src/com/jetbrains/python/testing/doctest/PythonDocTestCommandLineState.kt index fc17c2a6ec8e..ab7566143a97 100644 --- a/python/src/com/jetbrains/python/testing/doctest/PythonDocTestCommandLineState.kt +++ b/python/src/com/jetbrains/python/testing/doctest/PythonDocTestCommandLineState.kt @@ -9,8 +9,10 @@ import com.intellij.execution.target.value.TargetEnvironmentFunction import com.intellij.execution.target.value.plus import com.intellij.execution.target.value.targetPath import com.jetbrains.python.PythonHelper +import com.jetbrains.python.run.PythonScriptExecution import com.jetbrains.python.testing.AbstractPythonLegacyTestRunConfiguration.TestType.* import com.jetbrains.python.testing.PythonTestCommandLineStateBase +import org.jetbrains.annotations.TestOnly import java.nio.file.Path import java.util.function.Function @@ -26,6 +28,16 @@ class PythonDocTestCommandLineState(config: PythonDocTestRunConfiguration, env: override fun getTestSpecs(request: TargetEnvironmentRequest): List> = listOf(configuration.buildTestSpec(request)) + override fun addAfterParameters(targetEnvironmentRequest: TargetEnvironmentRequest, + testScriptExecution: PythonScriptExecution) { + configuration.parametersList.forEach { param -> testScriptExecution.addParameter(param) } + } + + @TestOnly + override fun addAfterParameters(cmd: GeneralCommandLine) { + cmd.parametersList.getParamsGroup(GROUP_SCRIPT)?.addParameters(configuration.parametersList) + } + companion object { /** * *To be deprecated. The part of the legacy implementation based on [GeneralCommandLine].* diff --git a/python/src/com/jetbrains/python/testing/doctest/PythonDocTestRunConfiguration.java b/python/src/com/jetbrains/python/testing/doctest/PythonDocTestRunConfiguration.java index 21fb24f71421..86804624022e 100644 --- a/python/src/com/jetbrains/python/testing/doctest/PythonDocTestRunConfiguration.java +++ b/python/src/com/jetbrains/python/testing/doctest/PythonDocTestRunConfiguration.java @@ -7,16 +7,45 @@ import com.intellij.execution.Executor; import com.intellij.execution.configurations.ConfigurationFactory; import com.intellij.execution.configurations.RunProfileState; import com.intellij.execution.runners.ExecutionEnvironment; +import com.intellij.execution.util.ProgramParametersConfigurator; import com.intellij.openapi.options.SettingsEditor; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.NlsSafe; import com.jetbrains.python.testing.AbstractPythonLegacyTestRunConfiguration; import org.jetbrains.annotations.NotNull; +import java.util.ArrayList; +import java.util.List; + public class PythonDocTestRunConfiguration extends AbstractPythonLegacyTestRunConfiguration implements PythonDocTestRunConfigurationParams { protected @NlsSafe String myPluralTitle = "Doctests"; protected @NlsSafe String myTitle = "Doctest"; + + private String myParameters = ""; + + private boolean isParametersEnabled = true; + + public final @NotNull List getParametersList() { + return isParametersEnabled ? ProgramParametersConfigurator.expandMacrosAndParseParameters(myParameters) : new ArrayList<>(); + } + + public final @NotNull String getParametersString() { + return isParametersEnabled ? myParameters : ""; + } + + public void addParameters(@NotNull String parameters) { + myParameters = parameters; + } + + public void setParametersEnabled(boolean isEnabled) { + isParametersEnabled = isEnabled; + } + + public boolean isParametersEnabled() { + return isParametersEnabled; + } + public PythonDocTestRunConfiguration(Project project, ConfigurationFactory configurationFactory) { super(project, configurationFactory); diff --git a/python/src/com/jetbrains/python/testing/doctest/PythonDocTestRunConfigurationEditor.java b/python/src/com/jetbrains/python/testing/doctest/PythonDocTestRunConfigurationEditor.java index d05eaa0f3cc4..ed6ab42f6e20 100644 --- a/python/src/com/jetbrains/python/testing/doctest/PythonDocTestRunConfigurationEditor.java +++ b/python/src/com/jetbrains/python/testing/doctest/PythonDocTestRunConfigurationEditor.java @@ -33,6 +33,7 @@ public class PythonDocTestRunConfigurationEditor extends SettingsEditor>> factorial(5) +120 +""" + + +def factorial(n): + """Return the factorial of n, an exact integer >= 0. + + >>> [factorial(n) for n in range(6)] + [1, 1, 2, 6, 24, 120] + >>> factorial(30) + 265252859812191058636308480000000 + >>> factorial(-1) + Traceback (most recent call last): + ... + ValueError: n must be >= 0 + + Factorials of floats are OK, but the float must be an exact integer: + >>> factorial(30.1) + Traceback (most recent call last): + ... + ValueError: n must be exact integer + >>> factorial(30.0) + 265252859812191058636308480000000 + + It must also not be ridiculously large: + >>> factorial(1e100) + Traceback (most recent call last): + ... + OverflowError: n too large + """ + + import math + if not n >= 0: + raise ValueError("n must be >= 0") + if math.floor(n) != n: + raise ValueError("n must be exact integer") + if n+1 == n: # catch a value like 1e300 + raise OverflowError("n too large") + result = 1 + factor = 2 + while factor <= n: + result *= factor + factor += 1 + return result + + +if __name__ == "__main__": + import doctest + doctest.testmod() diff --git a/python/testSrc/com/jetbrains/env/debug/PythonDoctestDebuggingTest.kt b/python/testSrc/com/jetbrains/env/debug/PythonDoctestDebuggingTest.kt new file mode 100644 index 000000000000..62da52d29ecc --- /dev/null +++ b/python/testSrc/com/jetbrains/env/debug/PythonDoctestDebuggingTest.kt @@ -0,0 +1,168 @@ +// 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.env.debug + +import com.intellij.execution.RunManager.Companion.getInstance +import com.intellij.execution.configurations.ConfigurationFactory +import com.intellij.execution.configurations.RunProfile +import com.intellij.execution.executors.DefaultDebugExecutor +import com.intellij.execution.runners.ProgramRunner +import com.intellij.openapi.projectRoots.Sdk +import com.intellij.testFramework.UsefulTestCase +import com.jetbrains.env.PyEnvTestCase +import com.jetbrains.env.PyTestTask +import com.jetbrains.python.debugger.PyDebugRunner +import com.jetbrains.python.run.AbstractPythonRunConfiguration +import com.jetbrains.python.run.CommandLinePatcher +import com.jetbrains.python.run.PythonCommandLineState +import com.jetbrains.python.testing.PythonTestConfigurationType +import com.jetbrains.python.testing.doctest.PythonDocTestRunConfiguration +import org.junit.Assert +import org.junit.Assume +import org.junit.Test + +class PythonDoctestDebuggingTest : PyEnvTestCase() { + companion object { + private const val VERBOSE_KEY = "-v" + private const val FAIL_FAST_KEY = "-f" + private const val DONT_ACCEPT_TRUE_FOR_1 = "-o DONT_ACCEPT_TRUE_FOR_1" + private const val NORMALIZE_WHITESPACE = "-o NORMALIZE_WHITESPACE" + + private fun concatAllParameters(vararg params: String): String { + val result = StringBuilder() + params.forEach { + result.append("$it ") + } + + result.deleteCharAt(result.length - 1) + return result.toString() + } + } + + private class DoctestDebuggingTask(scriptName: String = DEFAULT_TEST_FILE, parameters: String? = null) : PyCustomConfigDebuggerTask(RELATIVE_PATH) { + private val myParameters: String? + + init { + setScriptName(scriptName) + myParameters = parameters + } + + constructor(parameters: String) : this(DEFAULT_TEST_FILE, parameters) + + private fun defaultBefore() { + addBreakpoint(DEFAULT_BREAKPOINT) + myParameters?.let { addParameters(it) } + } + + private fun defaultTesting() { + waitForPause() + consoleExec(CONSOLE_EXEC) + waitForOutput(CONSOLE_EXEC) + removeBreakpoint(DEFAULT_BREAKPOINT) + resume() + waitForTerminate() + checkPydevWarning() + assertVerbose() + } + + fun addBreakpoint(line: Int) { + toggleBreakpoint(getFilePath(scriptName), line) + } + + fun removeBreakpoint(line: Int) { + removeBreakpoint(getFilePath(scriptName), line) + } + + fun addParameters(params: String) { + (myRunConfiguration as? PythonDocTestRunConfiguration)?.addParameters(params) + } + + override fun before() { + defaultBefore() + } + + override fun testing() { + defaultTesting() + } + + fun assertVerbose() { + if (myParameters != null && myParameters.contains(VERBOSE_KEY)) { + Assert.assertTrue(output().contains(RESULT)) + } + } + + fun checkPydevWarning() { + Assert.assertFalse(output().contains(PYDEV_WARNING)) + } + + override fun createRunConfiguration(sdkHome: String, existingSdk: Sdk?): AbstractPythonRunConfiguration<*> { + val factory: ConfigurationFactory = PythonTestConfigurationType.getInstance().docTestFactory + val runConfiguration = PythonDocTestRunConfiguration(project, factory) + val runner = ProgramRunner.getRunner(executorId, runConfiguration) as PyDebugRunner? + val executor = DefaultDebugExecutor.getDebugExecutorInstance() + runConfiguration.sdkHome = sdkHome + runConfiguration.sdk = existingSdk + runConfiguration.module = myFixture.module + runConfiguration.workingDirectory = myFixture.tempDirPath + runConfiguration.scriptName = scriptName + Assert.assertTrue(runner!!.canRun(executor.id, runConfiguration)) + mySettings = getInstance(project).createConfiguration(scriptName + javaClass.name + + "_RunConfiguration", factory) + return runConfiguration + } + + override fun createCommandLinePatchers(runner: PyDebugRunner, pyState: PythonCommandLineState, + profile: RunProfile, serverLocalPort: Int): Array { + return runner.createCommandLinePatchers(project, pyState, profile, serverLocalPort) + } + + companion object { + private const val RELATIVE_PATH = "/debug/" + private const val DEFAULT_TEST_FILE = "test_doctest.py" + private const val DEFAULT_BREAKPOINT = 45 + private const val PYDEV_WARNING = "PYDEV DEBUGGER WARNING:\nsys.settrace() should not be used when the debugger is being used." + private val RESULT = """ + Trying: + [factorial(n) for n in range(6)] + Expecting: + [1, 1, 2, 6, 24, 120] + """.trimIndent() + private const val CONSOLE_EXEC = "_testing_doctest_" + } + } + + override fun runPythonTest(testTask: PyTestTask?) { + // Don't run on TeamCity because of PY-45432. + Assume.assumeFalse(UsefulTestCase.IS_UNDER_TEAMCITY) + super.runPythonTest(testTask) + } + + @Test + fun testSimple() { + runPythonTest(DoctestDebuggingTask()) + } + + @Test + fun testWithVerbose() { + runPythonTest(DoctestDebuggingTask(VERBOSE_KEY)) + } + + @Test + fun testWithFailFast() { + runPythonTest(DoctestDebuggingTask(FAIL_FAST_KEY)) + } + + @Test + fun testWithOptionKey() { + runPythonTest(DoctestDebuggingTask(DONT_ACCEPT_TRUE_FOR_1)) + } + + @Test + fun testWithOptionKeys() { + runPythonTest(DoctestDebuggingTask(concatAllParameters(DONT_ACCEPT_TRUE_FOR_1, NORMALIZE_WHITESPACE))) + } + + @Test + fun testWithAllParameters() { + runPythonTest(DoctestDebuggingTask(concatAllParameters(VERBOSE_KEY, FAIL_FAST_KEY, DONT_ACCEPT_TRUE_FOR_1, NORMALIZE_WHITESPACE))) + } +} \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/env/python/testing/PythonDocTestingTest.java b/python/testSrc/com/jetbrains/env/python/testing/PythonDocTestingTest.java index fe86f2aa14b2..5fc88c436eaa 100644 --- a/python/testSrc/com/jetbrains/env/python/testing/PythonDocTestingTest.java +++ b/python/testSrc/com/jetbrains/env/python/testing/PythonDocTestingTest.java @@ -13,7 +13,7 @@ import org.junit.Test; import java.io.IOException; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; /** * User : catherine @@ -180,4 +180,61 @@ public final class PythonDocTestingTest extends PyEnvTestCase { } }); } + + @Test + public void testWithParameters() { + runPythonTest(new PyDocTestWithParametersTask("-v")); + runPythonTest(new PyDocTestWithParametersTask("-f")); + runPythonTest(new PyDocTestWithParametersTask("-o DONT_ACCEPT_TRUE_FOR_1")); + runPythonTest(new PyDocTestWithParametersTask("-o NORMALIZE_WHITESPACE")); + runPythonTest(new PyDocTestWithParametersTask("-o NORMALIZE_WHITESPACE -o DONT_ACCEPT_TRUE_FOR_1")); + runPythonTest(new PyDocTestWithParametersTask("-v -f -o NORMALIZE_WHITESPACE -o DONT_ACCEPT_TRUE_FOR_1")); + } + + private static class PyDocTestWithParametersTask extends PyProcessWithConsoleTestTask { + private static final String RESULT = """ + Trying: + [factorial(n) for n in range(6)] + Expecting: + [1, 1, 2, 6, 24, 120]"""; + + private static final String VERBOSE_KEY = "-v"; + private static final String RELATIVE_TEST_DATA_PATH = "/testRunner/env/doc"; + private static final SdkCreationType REQUIRED_SDK_TYPE = SdkCreationType.EMPTY_SDK; + + private final String parametersString; + + protected PyDocTestWithParametersTask(String params) { + super(RELATIVE_TEST_DATA_PATH, REQUIRED_SDK_TYPE); + parametersString = params; + } + + @NotNull + @Override + protected PyDocTestProcessRunner createProcessRunner() { + return new PyDocTestProcessRunner("test1.py", 0) { + @Override + protected void configurationCreatedAndWillLaunch(@NotNull PythonDocTestRunConfiguration configuration) throws IOException { + super.configurationCreatedAndWillLaunch(configuration); + configuration.addParameters(parametersString); + } + }; + } + + private void assertVerbose(@NotNull final String stdout) { + if (parametersString.contains(VERBOSE_KEY)) { + assertTrue(stdout.contains(RESULT)); + } + } + + @Override + protected void checkTestResults(@NotNull final PyDocTestProcessRunner runner, + @NotNull final String stdout, + @NotNull final String stderr, + @NotNull final String all, int exitCode) { + assertVerbose(stdout); + assertEquals(0, exitCode); + assertTrue(stderr.isEmpty()); + } + } }