Files
openide/python/helpers/pycharm/docrunner.py
Egor.Eliseev fbf14c190d 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
2023-09-04 13:18:44 +00:00

414 lines
13 KiB
Python

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)
from tcunittest import TeamcityTestResult
from tcmessages import TeamcityServiceMessages
from pycharm_run_utils import import_system_module
from pycharm_run_utils import adjust_sys_path, debug, getModuleName, PYTHON_VERSION_MAJOR
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):
"""
DocTests Result extends TeamcityTestResult,
overrides some methods, specific for doc tests,
such as getTestName, getTestId.
"""
def getTestName(self, test):
name = self.current_suite.name + test.source
return name
def getSuiteName(self, suite):
if test.source.rfind(".") == -1:
name = self.current_suite.name + test.source
else:
name = test.source
return name
def getTestId(self, test):
file = os.path.realpath(self.current_suite.filename) if self.current_suite.filename else ""
line_no = test.lineno
if self.current_suite.lineno:
line_no += self.current_suite.lineno
return "file://" + file + ":" + str(line_no)
def getSuiteLocation(self):
file = os.path.realpath(self.current_suite.filename) if self.current_suite.filename else ""
location = "file://" + file
if self.current_suite.lineno:
location += ":" + str(self.current_suite.lineno)
return location
def startTest(self, test):
setattr(test, "startTime", datetime.datetime.now())
id = self.getTestId(test)
self.messages.testStarted(self.getTestName(test), location=id)
def startSuite(self, suite):
self.current_suite = suite
self.messages.testSuiteStarted(suite.name, location=self.getSuiteLocation())
def stopSuite(self, suite):
self.messages.testSuiteFinished(suite.name)
def addFailure(self, test, err = '', expected=None, actual=None):
self.messages.testFailed(self.getTestName(test), expected=expected, actual=actual,
message='Failure', details=err, duration=int(self.__getDuration(test)))
def addError(self, test, err = ''):
self.messages.testError(self.getTestName(test),
message='Error', details=err, duration=self.__getDuration(test))
def stopTest(self, test):
duration = self.__getDuration(test)
self.messages.testFinished(self.getTestName(test), duration=int(duration))
def __getDuration(self, test):
start = getattr(test, "startTime", datetime.datetime.now())
d = datetime.datetime.now() - start
duration = d.microseconds / 1000 + d.seconds * 1000 + d.days * 86400000
return duration
class DocTestRunner(doctest.DocTestRunner):
"""
Special runner for doctests,
overrides __run method to report results using TeamcityDocTestResult
"""
def __init__(self, checker=None, verbose=None, optionflags=0):
doctest.DocTestRunner.__init__(self, checker=checker, verbose=verbose, optionflags=optionflags)
self.stream = sys.stdout
self.result = TeamcityDocTestResult(self.stream)
#self.result.messages.testMatrixEntered()
self._tests = []
def addTests(self, tests):
self._tests.extend(tests)
def addTest(self, test):
self._tests.append(test)
def countTests(self):
return len(self._tests)
def start(self):
for test in self._tests:
self.run(test)
def __run(self, test, compileflags, out):
failures = tries = 0
original_optionflags = self.optionflags
SUCCESS, FAILURE, BOOM = range(3) # `outcome` state
check = self._checker.check_output
self.result.startSuite(test)
for examplenum, example in enumerate(test.examples):
quiet = (self.optionflags & doctest.REPORT_ONLY_FIRST_FAILURE and
failures > 0)
self.optionflags = original_optionflags
if example.options:
for (optionflag, val) in example.options.items():
if val:
self.optionflags |= optionflag
else:
self.optionflags &= ~optionflag
if hasattr(doctest, 'SKIP'):
if self.optionflags & doctest.SKIP:
continue
tries += 1
if not quiet:
self.report_start(out, test, example)
filename = '<doctest %s[%d]>' % (test.name, examplenum)
try:
exec(compile(example.source, filename, "single",
compileflags, 1), test.globs)
self.debugger.set_continue() # ==== Example Finished ====
exception = None
except KeyboardInterrupt:
raise
except:
exception = sys.exc_info()
self.debugger.set_continue() # ==== Example Finished ====
got = self._fakeout.getvalue() # the actual output
self._fakeout.truncate(0)
outcome = FAILURE # guilty until proved innocent or insane
if exception is None:
if check(example.want, got, self.optionflags):
outcome = SUCCESS
else:
exc_msg = traceback.format_exception_only(*exception[:2])[-1]
if not quiet:
got += doctest._exception_traceback(exception)
if example.exc_msg is None:
outcome = BOOM
elif check(example.exc_msg, exc_msg, self.optionflags):
outcome = SUCCESS
elif self.optionflags & doctest.IGNORE_EXCEPTION_DETAIL:
m1 = re.match(r'[^:]*:', example.exc_msg)
m2 = re.match(r'[^:]*:', exc_msg)
if m1 and m2 and check(m1.group(0), m2.group(0),
self.optionflags):
outcome = SUCCESS
# Report the outcome.
if outcome is SUCCESS:
self.result.startTest(example)
self.result.stopTest(example)
elif outcome is FAILURE:
self.result.startTest(example)
err = self._failure_header(test, example) +\
self._checker.output_difference(example, got, self.optionflags)
expected = getattr(example, "want", None)
self.result.addFailure(example, err, expected=expected, actual=got)
elif outcome is BOOM:
self.result.startTest(example)
err=self._failure_header(test, example) +\
'Exception raised:\n' + doctest._indent(doctest._exception_traceback(exception))
self.result.addError(example, err)
else:
assert False, ("unknown outcome", outcome)
self.optionflags = original_optionflags
self.result.stopSuite(test)
modules = {}
def _load_file(moduleName, fileName):
if sys.version_info >= (3, 5):
import importlib
return importlib.import_module(moduleName, fileName)
else:
import imp
return imp.load_source(moduleName, fileName)
def loadSource(fileName):
"""
loads source from fileName,
we can't use tat function from utrunner, because of we
store modules in global variable.
"""
baseName = os.path.basename(fileName)
moduleName = os.path.splitext(baseName)[0]
# for users wanted to run simple doctests under django
#because of django took advantage of module name
settings_file = os.getenv('DJANGO_SETTINGS_MODULE')
if settings_file and moduleName=="models":
baseName = os.path.realpath(fileName)
moduleName = ".".join((baseName.split(os.sep)[-2], "models"))
if moduleName in modules: # add unique number to prevent name collisions
cnt = 2
prefix = moduleName
while getModuleName(prefix, cnt) in modules:
cnt += 1
moduleName = getModuleName(prefix, cnt)
debug("/ Loading " + fileName + " as " + moduleName)
module = _load_file(moduleName, fileName)
modules[moduleName] = module
return module
def testfile(filename):
if PYTHON_VERSION_MAJOR == 3:
text, filename = doctest._load_testfile(filename, None, False, "utf-8")
else:
text, filename = doctest._load_testfile(filename, None, False)
name = os.path.basename(filename)
globs = {'__name__': '__main__'}
parser = doctest.DocTestParser()
# Read the file, convert it to a test, and run it.
test = parser.get_doctest(text, globs, name, filename, 0)
if test.examples:
runner.addTest(test)
def testFilesInFolder(folder):
return testFilesInFolderUsingPattern(folder)
def testFilesInFolderUsingPattern(folder, pattern = ".*"):
''' loads modules from folder ,
check if module name matches given pattern'''
modules = []
prog = re.compile(pattern)
for root, dirs, files in os.walk(folder):
for name in files:
path = os.path.join(root, name)
if prog.match(name):
if name.endswith(".py"):
modules.append(loadSource(path))
elif not name.endswith(".pyc") and not name.endswith("$py.class") and os.path.isfile(path):
testfile(path)
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:]:
arg = arg.strip()
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
a_splitted = a[0].split(";")
if len(a_splitted) != 1:
# means we have pattern to match against
if a_splitted[0].endswith("/"):
debug("/ from folder " + a_splitted[0] + ". Use pattern: " + a_splitted[1])
modules = testFilesInFolderUsingPattern(a_splitted[0], a_splitted[1])
else:
if a[0].endswith("/"):
debug("/ from folder " + a[0])
modules = testFilesInFolder(a[0])
else:
# from file
debug("/ from module " + a[0])
# for doctests from non-python file
if a[0].rfind(".py") == -1:
testfile(a[0])
modules = []
else:
modules = [loadSource(a[0])]
# for doctests
for module in modules:
tests = finder.find(module, module.__name__)
for test in tests:
if test.examples:
runner.addTest(test)
elif len(a) == 2:
# From testcase
debug("/ from class " + a[1] + " in " + a[0])
try:
module = loadSource(a[0])
except SyntaxError:
raise NameError('File "%s" is not python file' % (a[0], ))
if hasattr(module, a[1]):
testcase = getattr(module, a[1])
tests = finder.find(testcase, getattr(testcase, "__name__", None))
runner.addTests(tests)
else:
raise NameError('Module "%s" has no class "%s"' % (a[0], a[1]))
else:
# From method in class or from function
try:
module = loadSource(a[0])
except SyntaxError:
raise NameError('File "%s" is not python file' % (a[0], ))
if a[1] == "":
# test function, not method
debug("/ from method " + a[2] + " in " + a[0])
if hasattr(module, a[2]):
testcase = getattr(module, a[2])
tests = finder.find(testcase, getattr(testcase, "__name__", None))
runner.addTests(tests)
else:
raise NameError('Module "%s" has no method "%s"' % (a[0], a[2]))
else:
debug("/ from method " + a[2] + " in class " + a[1] + " in " + a[0])
if hasattr(module, a[1]):
testCaseClass = getattr(module, a[1])
if hasattr(testCaseClass, a[2]):
testcase = getattr(testCaseClass, a[2])
name = getattr(testcase, "__name__", None)
if not name:
name = testCaseClass.__name__
tests = finder.find(testcase, name)
runner.addTests(tests)
else:
raise NameError('Class "%s" has no function "%s"' % (testCaseClass, a[2]))
else:
raise NameError('Module "%s" has no class "%s"' % (module, a[1]))
debug("/ Loaded " + str(runner.countTests()) + " tests")
TeamcityServiceMessages(sys.stdout).testCount(runner.countTests())
runner.start()