PY-43327 Add parameters to doctest configuration

Remove pydev warning if old trace function equals to new one.
Add argparse for `docrunner.py`.
Add tests for `doctest` with parameters.
Add debugger tests for `doctest` with parameters.


Merge-request: IJ-MR-111959
Merged-by: Egor Eliseev <Egor.Eliseev@jetbrains.com>

GitOrigin-RevId: 608080b3b5db93718f62a3dddd17893fe8118539
This commit is contained in:
Egor.Eliseev
2023-09-04 13:18:44 +00:00
committed by intellij-monorepo-bot
parent ada78a6252
commit fbf14c190d
10 changed files with 424 additions and 7 deletions
+57 -4
View File
@@ -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
+1 -1
View File
@@ -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()
@@ -284,6 +284,10 @@ public class PythonTestLegacyRunConfigurationForm implements AbstractPythonTestR
return myParamCheckBox;
}
public JTextField getParamTextField() {
return myParamTextField;
}
public void setParams(String params) {
myParamTextField.setText(params);
}
@@ -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<Function<TargetEnvironment, String>> =
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].*
@@ -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<PythonDocTestRunConfiguration>
implements PythonDocTestRunConfigurationParams {
protected @NlsSafe String myPluralTitle = "Doctests";
protected @NlsSafe String myTitle = "Doctest";
private String myParameters = "";
private boolean isParametersEnabled = true;
public final @NotNull List<String> 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);
@@ -33,6 +33,7 @@ public class PythonDocTestRunConfigurationEditor extends SettingsEditor<PythonDo
@Override
protected void disposeEditor() {
myForm.removeListeners();
myForm = null;
}
}
@@ -16,27 +16,65 @@
package com.jetbrains.python.testing.doctest;
import com.intellij.openapi.project.Project;
import com.intellij.ui.DocumentAdapter;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.testing.AbstractPythonTestRunConfigurationParams;
import com.jetbrains.python.testing.PyTestSharedForm;
import com.jetbrains.python.testing.PythonTestLegacyRunConfigurationForm;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.*;
public class PythonDocTestRunConfigurationForm implements PythonDocTestRunConfigurationParams {
private final JPanel myRootPanel;
private final PythonTestLegacyRunConfigurationForm myTestRunConfigurationForm;
private final DocumentListener myParamTextFieldListener;
private final ChangeListener myParamCheckBoxListener;
public PythonDocTestRunConfigurationForm(final Project project, final PythonDocTestRunConfiguration configuration) {
myRootPanel = new JPanel(new BorderLayout());
myTestRunConfigurationForm = new PythonTestLegacyRunConfigurationForm(project, configuration);
PyTestSharedForm.setBorderToPanel(myTestRunConfigurationForm.getTestsPanel(), PyBundle.message("runcfg.doctest.display_name"));
myParamTextFieldListener = new DocumentAdapter() {
@Override
protected void textChanged(@NotNull DocumentEvent e) {
configuration.addParameters(myTestRunConfigurationForm.getParamTextField().getText());
}
};
myParamCheckBoxListener = new ChangeListener() {
@Override
public void stateChanged(@NotNull ChangeEvent e) {
if (e.getSource() instanceof JCheckBox checkBox) {
configuration.setParametersEnabled(checkBox.isSelected());
}
}
};
addParameters(configuration);
myRootPanel.add(myTestRunConfigurationForm.getPanel(), BorderLayout.CENTER);
}
private void addParameters(PythonDocTestRunConfiguration configuration) {
myTestRunConfigurationForm.setParamsVisible();
myTestRunConfigurationForm.getParamCheckBox().setSelected(configuration.isParametersEnabled());
myTestRunConfigurationForm.getParamTextField().setText(configuration.getParametersString());
myTestRunConfigurationForm.getParamTextField().getDocument().addDocumentListener(myParamTextFieldListener);
myTestRunConfigurationForm.getParamCheckBox().addChangeListener(myParamCheckBoxListener);
}
public void removeListeners() {
myTestRunConfigurationForm.getParamTextField().getDocument().removeDocumentListener(myParamTextFieldListener);
myTestRunConfigurationForm.getParamCheckBox().removeChangeListener(myParamCheckBoxListener);
}
@Override
public AbstractPythonTestRunConfigurationParams getTestRunConfigurationParams() {
return myTestRunConfigurationForm;
+55
View File
@@ -0,0 +1,55 @@
"""
This is the "example" module.
The example module supplies one function, factorial(). For example,
>>> 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()
@@ -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<CommandLinePatcher> {
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)))
}
}
@@ -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<PyDocTestProcessRunner> {
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());
}
}
}