diff --git a/Lib/test/regrtest.py b/Lib/test/regrtest.py --- a/Lib/test/regrtest.py +++ b/Lib/test/regrtest.py @@ -52,6 +52,9 @@ --testdir DIR -- execute test files in the specified directory (instead of the Python stdlib test suite) + --doctest -- run in doctest mode. + --docdir DIR + -- path to documentation directory (containing *.rst files). Special runs @@ -169,6 +172,7 @@ import importlib import builtins +import doctest import faulthandler import getopt import io @@ -306,7 +310,7 @@ 'runleaks', 'huntrleaks=', 'memlimit=', 'randseed=', 'multiprocess=', 'coverage', 'slaveargs=', 'forever', 'debug', 'start=', 'nowindows', 'header', 'testdir=', 'timeout=', 'wait', - 'failfast', 'match']) + 'failfast', 'match', 'doctests', 'docdir=']) except getopt.error as msg: usage(msg) @@ -318,6 +322,8 @@ debug = False start = None timeout = None + run_doctests = False + docdir = None for o, a in opts: if o in ('-h', '--help'): print(__doc__) @@ -449,6 +455,10 @@ print() # Force a newline (just in case) print(json.dumps(result)) sys.exit(0) + elif o == '--doctests': + run_doctests = True + elif o == '--docdir': + docdir = a elif o == '--testdir': # CWD is replaced with a temporary dir before calling main(), so we # join it with the saved CWD so it ends up where the user expects. @@ -542,9 +552,13 @@ print("== ", os.getcwd()) print("Testing with flags:", sys.flags) - # if testdir is set, then we are not running the python tests suite, so - # don't add default tests to be executed or skipped (pass empty values) - if testdir: + if run_doctests: + # Then include only doctests. + alltests = find_doctests(docdir=docdir) + elif testdir: + # If testdir is set, then we are not running the python test suite, + # so don't add default tests to be executed or skipped (pass empty + # values). alltests = findtests(testdir, list(), set()) else: alltests = findtests(testdir, stdtests, nottests) @@ -713,7 +727,7 @@ result = runtest(test, verbose, quiet, huntrleaks, debug, output_on_failure=verbose3, timeout=timeout, failfast=failfast, - match_tests=match_tests) + match_tests=match_tests, docdir=docdir) accumulate_result(test, result) except KeyboardInterrupt: interrupted = True @@ -784,7 +798,8 @@ sys.stdout.flush() try: verbose = True - ok = runtest(test, True, quiet, huntrleaks, debug, timeout=timeout) + ok = runtest(test, True, quiet, huntrleaks, debug, + timeout=timeout, docdir=docdir) except KeyboardInterrupt: # print a newline separate from the ^C print() @@ -827,6 +842,96 @@ # set of tests that we don't want to be executed when using regrtest NOTTESTS = set() +def path_split(path): + """Split the path into parts using os.path.split().""" + parts = [] + while True: + path, tail = os.path.split(path) + parts.append(tail) + if not path: + break + parts.reverse() + return parts + +def find_files(top_dir, include_file=None, include_dir=None): + """Return the relative paths to files inside top_dir.""" + # Include all files and directories by default. + if include_file is None: + include_file = lambda path: True + if include_dir is None: + include_dir = lambda path: True + + def get_paths(rel_dir, names, include): + paths = [] + for name in names: + # Call normpath() to collapse the leading "./", for example. + path = os.path.normpath(os.path.join(rel_dir, name)) + if not include(path): + continue + paths.append(path) + return paths + + paths = [] + if top_dir is None: + return paths + for (dirpath, dirnames, filenames) in os.walk(top_dir): + rel_dir = os.path.relpath(dirpath, top_dir) + # For example, to collapse the leading "./". + rel_dir = os.path.normpath(rel_dir) + if not include_dir(rel_dir): + continue + paths.extend(get_paths(rel_dir, filenames, include_file)) + paths.sort() + return paths + +def find_doc_paths(docdir=None): + """Return a list of relative paths to all documentation files.""" + include_file = lambda path: path.endswith(".rst") + return find_files(docdir, include_file=include_file) + +def find_mod_paths(): + """Return a list of relative paths to non-test Library modules.""" + def include_dir(path): + parts = path_split(path) + if "test" in parts or "tests" in parts: + return False + basename = os.path.basename(path) + if basename == "__pycache__": + return False + return True + + def include_file(path): + filename = os.path.basename(path) + if filename == "__main__.py": + return False + mod, ext = os.path.splitext(filename) + return ext == ".py" + + libdir = findlibdir() + return find_files(libdir, include_file=include_file, + include_dir=include_dir) + +def find_doctests(docdir=None): + """Return a list of test names for all doctests.""" + tests = [] + if docdir is not None: + doc_paths = find_doc_paths(docdir=docdir) + # Add file doctests. + tests.extend(["fdoc:" + path for path in doc_paths]) + + mod_paths = find_mod_paths() + for path in mod_paths: + mod_base, ext = os.path.splitext(path) + parts = path_split(mod_base) + if parts[-1] == "__init__": + parts.pop() + mod_name = ".".join(parts) + # Add a module doctest. + tests.append("mdoc:" + mod_name) + + tests.sort() + return tests + def findtests(testdir=None, stdtests=STDTESTS, nottests=NOTTESTS): """Return a list of all applicable test modules.""" testdir = findtestdir(testdir) @@ -878,7 +983,7 @@ def runtest(test, verbose, quiet, huntrleaks=False, debug=False, use_resources=None, output_on_failure=False, failfast=False, match_tests=None, - timeout=None): + timeout=None, docdir=None): """Run a single test. test -- the name of the test @@ -929,7 +1034,8 @@ sys.stdout = stream sys.stderr = stream result = runtest_inner(test, verbose, quiet, huntrleaks, - debug, display_failure=False) + debug, display_failure=False, + docdir=docdir) if result[0] == FAILED: output = stream.getvalue() orig_stderr.write(output) @@ -940,7 +1046,7 @@ else: support.verbose = verbose # Tell tests to be moderately quiet result = runtest_inner(test, verbose, quiet, huntrleaks, debug, - display_failure=not verbose) + display_failure=not verbose, docdir=docdir) return result finally: if use_timeout: @@ -1195,33 +1301,88 @@ file=sys.stderr) return False +def make_test_runner(test_suite): + return lambda: support.run_unittest(test_suite) + +def load_file_doc_test_runner(path, docdir=None): + """Return a test runner for doctests in the given doc file. + + """ + if docdir is not None: + path = os.path.join(docdir, path) + try: + suite = doctest.DocFileSuite(path, module_relative=False) + except FileNotFoundError: + raise + except Exception as err: + raise err.__class__("Error creating DocFileSuite from: %s" % + repr(path)) from err + return make_test_runner(suite) + +def load_module_doc_test_runner(mod_name): + """Return a test runner for doctests in the given module. + + """ + mod = __import__(mod_name, globals(), locals(), []) + suite = unittest.TestSuite() + if getattr(mod, "_doctest_safe", None): + # We pass a test_finder to prevent a ValueError from being thrown + # in case the module has no docstrings. See also issue #14649. + finder = doctest.DocTestFinder(exclude_empty=False) + suite = doctest.DocTestSuite(mod, test_finder=finder) + return make_test_runner(suite) + +def load_test_module_test_runner(test): + """Return a callable test runner for the given test. + + The argument *test* is the name of the test module. + + """ + if test.startswith('test.'): + abstest = test + else: + # Always import it from the test package + abstest = 'test.' + test + the_package = __import__(abstest, globals(), locals(), []) + the_module = getattr(the_package, test) + # If the test has a test_main, that will run the appropriate + # tests. If not, use normal unittest test loading. + test_runner = getattr(the_module, "test_main", None) + if test_runner is None: + tests = unittest.TestLoader().loadTestsFromModule(the_module) + test_runner = make_test_runner(tests) + + return test_runner + +def load_test_runner(test, docdir=None): + """Return a callable test runner for the given test. + + The argument *test* is the name of the test module. + + """ + fprefix, mprefix = "fdoc:", "mdoc:" + if test.startswith(fprefix): + runner = load_file_doc_test_runner(test[len(fprefix):], docdir=docdir) + elif test.startswith(mprefix): + runner = load_module_doc_test_runner(test[len(mprefix):]) + else: + runner = load_test_module_test_runner(test) + return runner def runtest_inner(test, verbose, quiet, - huntrleaks=False, debug=False, display_failure=True): + huntrleaks=False, debug=False, display_failure=True, + docdir=None): support.unload(test) test_time = 0.0 refleak = False # True if the test leaked references. try: - if test.startswith('test.'): - abstest = test - else: - # Always import it from the test package - abstest = 'test.' + test with saved_test_environment(test, verbose, quiet) as environment: start_time = time.time() - the_package = __import__(abstest, globals(), locals(), []) - the_module = getattr(the_package, test) - # If the test has a test_main, that will run the appropriate - # tests. If not, use normal unittest test loading. - test_runner = getattr(the_module, "test_main", None) - if test_runner is None: - tests = unittest.TestLoader().loadTestsFromModule(the_module) - test_runner = lambda: support.run_unittest(tests) + test_runner = load_test_runner(test, docdir=docdir) test_runner() if huntrleaks: - refleak = dash_R(the_module, test, test_runner, - huntrleaks) + refleak = dash_R(None, test, test_runner, huntrleaks) test_time = time.time() - start_time except support.ResourceDenied as msg: if not quiet: @@ -1440,6 +1601,11 @@ # int cache x = list(range(-5, 257)) +def findlibdir(): + """Return the path to the source Lib directory.""" + # We can choose any Python module in the standard library. + return os.path.dirname(sysconfig.__file__) + def findtestdir(path=None): return path or os.path.dirname(__file__) or os.curdir