mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-08 23:39:39 +07:00
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
414 lines
13 KiB
Python
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()
|