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 = '' % (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()