| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent e2a2251 commit fe0fe87
6 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,5 +1,3 @@ | |||
| 1 | - build | ||
| 2 | 1 | *.swp | |
| 3 | - tests/build/ | ||
| 4 | 2 | *.py[cod] | |
| 5 | 3 | __pycache__ | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,7 +2,8 @@ Changelog | |||
| 2 | 2 | ========= | |
| 3 | 3 | ||
| 4 | 4 | * 2026-10-10: Add ``PyObject_GenericHash()`` and | |
| 5 | - ``PyUnicodeWriter_WriteUCS4()`` functions. | ||
| 5 | + ``PyUnicodeWriter_WriteUCS4()`` functions. runtests.py now runs tests in | ||
| 6 | + parallel. | ||
| 6 | 7 | * 2026-10-08: Add support for the limited C API 3.2. | |
| 7 | 8 | * 2026-10-08: Add functions: | |
| 8 | 9 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -13,11 +13,9 @@ | |||
| 13 | 13 | import shutil | |
| 14 | 14 | import sys | |
| 15 | 15 | import time | |
| 16 | - from shutil import which | ||
| 17 | - | ||
| 18 | - | ||
| 19 | - from tests.utils import run_command | ||
| 16 | + from concurrent.futures import ThreadPoolExecutor | ||
| 20 | 17 | ||
| 18 | + from tests.utils import get_output, run_command | ||
| 21 | 19 | ||
| 22 | 20 | TEST_DIR = os.path.join(os.path.dirname(__file__), 'tests') | |
| 23 | 21 | TEST_COMPAT = os.path.join(TEST_DIR, "test_pythoncapi_compat.py") | |
@@ -53,26 +51,13 @@ | |||
| 53 | 51 | ) | |
| 54 | 52 | ||
| 55 | 53 | ||
| 56 | - def run_tests_exe(executable, verbose, tested): | ||
| 57 | - tested_key = os.path.realpath(executable) | ||
| 58 | - if tested_key in tested: | ||
| 59 | - return | ||
| 60 | - | ||
| 54 | + def get_test_command(executable, verbose): | ||
| 61 | 55 | # Don't use realpath() for the executed command to support virtual | |
| 62 | 56 | # environments | |
| 63 | 57 | cmd = [executable, TEST_COMPAT] | |
| 64 | 58 | if verbose: | |
| 65 | 59 | cmd.append('-v') | |
| 66 | - run_command(cmd) | ||
| 67 | - tested.add(tested_key) | ||
| 68 | - | ||
| 69 | - | ||
| 70 | - def run_tests(python, verbose, tested): | ||
| 71 | - executable = which(python) | ||
| 72 | - if not executable: | ||
| 73 | - print("Ignore missing Python executable: %s" % python) | ||
| 74 | - return | ||
| 75 | - run_tests_exe(executable, verbose, tested) | ||
| 60 | + return cmd | ||
| 76 | 61 | ||
| 77 | 62 | ||
| 78 | 63 | def parse_args(): | |
@@ -85,35 +70,74 @@ def parse_args(): | |||
| 85 | 70 | return parser.parse_args() | |
| 86 | 71 | ||
| 87 | 72 | ||
| 88 | - def main(): | ||
| 89 | - start_time = time.perf_counter() | ||
| 90 | - args = parse_args() | ||
| 73 | + def run_tests_parallel(args): | ||
| 74 | + jobs = [] | ||
| 75 | + | ||
| 76 | + tested = set() | ||
| 77 | + tested_key = os.path.realpath(sys.executable) | ||
| 78 | + tested.add(tested_key) | ||
| 79 | + jobs.append(sys.executable) | ||
| 80 | + | ||
| 81 | + for python in PYTHONS: | ||
| 82 | + executable = shutil.which(python) | ||
| 83 | + if not executable: | ||
| 84 | + print(f"Ignore missing Python executable: {python}") | ||
| 85 | + continue | ||
| 86 | + tested_key = os.path.realpath(executable) | ||
| 87 | + if tested_key in tested: | ||
| 88 | + continue | ||
| 89 | + tested.add(tested_key) | ||
| 90 | + jobs.append(executable) | ||
| 91 | + | ||
| 92 | + def worker(executable): | ||
| 93 | + cmd = get_test_command(executable, args.verbose) | ||
| 94 | + return get_output(cmd) | ||
| 95 | + | ||
| 96 | + if hasattr(os, 'process_cpu_count'): | ||
| 97 | + max_workers = os.process_cpu_count() | ||
| 98 | + else: | ||
| 99 | + max_workers = os.cpu_count() | ||
| 100 | + | ||
| 101 | + print() | ||
| 102 | + print(f"Run {len(jobs)} jobs with {max_workers} workers (threads)") | ||
| 103 | + print() | ||
| 104 | + | ||
| 105 | + with ThreadPoolExecutor(max_workers=max_workers) as executor: | ||
| 106 | + exitcode = None | ||
| 107 | + for exitcode, stdout in executor.map(worker, jobs): | ||
| 108 | + print(stdout, end='') | ||
| 109 | + if exitcode: | ||
| 110 | + break | ||
| 91 | 111 | ||
| 92 | - path = os.path.join(TEST_DIR, 'build') | ||
| 93 | - if os.path.exists(path): | ||
| 94 | - shutil.rmtree(path) | ||
| 112 | + if exitcode: | ||
| 113 | + executor.shutdown(wait=True, cancel_futures=True) | ||
| 114 | + sys.exit(exitcode) | ||
| 95 | 115 | ||
| 116 | + print() | ||
| 117 | + print(f"Tested: {len(jobs)} Python executables") | ||
| 118 | + | ||
| 119 | + | ||
| 120 | + def test_upgrade_pythoncapi(args): | ||
| 96 | 121 | # upgrade_pythoncapi.py requires Python 3.6 or newer | |
| 97 | - if sys.version_info >= (3, 6): | ||
| 98 | - print("Run %s" % TEST_UPGRADE) | ||
| 99 | - cmd = [sys.executable, TEST_UPGRADE] | ||
| 100 | - if args.verbose: | ||
| 101 | - cmd.append('-v') | ||
| 102 | - run_command(cmd) | ||
| 103 | - else: | ||
| 104 | - print("Don't test upgrade_pythoncapi.py: it requires Python 3.6") | ||
| 122 | + print(f"Run {TEST_UPGRADE}") | ||
| 123 | + cmd = [sys.executable, TEST_UPGRADE] | ||
| 124 | + if args.verbose: | ||
| 125 | + cmd.append('-v') | ||
| 126 | + run_command(cmd) | ||
| 105 | 127 | print() | |
| 106 | 128 | ||
| 107 | - tested = set() | ||
| 108 | - if not args.current: | ||
| 109 | - for python in PYTHONS: | ||
| 110 | - run_tests(python, args.verbose, tested) | ||
| 111 | - run_tests_exe(sys.executable, args.verbose, tested) | ||
| 112 | 129 | ||
| 113 | - print() | ||
| 114 | - print("Tested: %s Python executables" % len(tested)) | ||
| 130 | + def main(): | ||
| 131 | + start_time = time.perf_counter() | ||
| 132 | + args = parse_args() | ||
| 133 | + | ||
| 134 | + test_upgrade_pythoncapi(args) | ||
| 135 | + | ||
| 136 | + if not args.current: | ||
| 137 | + run_tests_parallel(args) | ||
| 115 | 138 | else: | |
| 116 | - run_tests_exe(sys.executable, args.verbose, tested) | ||
| 139 | + cmd = get_test_command(sys.executable, args.verbose) | ||
| 140 | + run_command(cmd) | ||
| 117 | 141 | print() | |
| 118 | 142 | ||
| 119 | 143 | dt = time.perf_counter() - start_time | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -28,7 +28,7 @@ | |||
| 28 | 28 | else: | |
| 29 | 29 | TEST_OLD_LIMITED_C_API = False | |
| 30 | 30 | ||
| 31 | - OLD_LIMITED_CAPI = f'-DPy_LIMITED_API=0x3020000' | ||
| 31 | + OLD_LIMITED_CAPI = '-DPy_LIMITED_API=0x3020000' | ||
| 32 | 32 | ||
| 33 | 33 | SRC_DIR = os.path.normpath(os.path.join(os.path.dirname(__file__), '..')) | |
| 34 | 34 | LIMITED_SUFFIX = "_limited" | |
@@ -136,9 +136,9 @@ def cxx_extension_name(std): | |||
| 136 | 136 | ||
| 137 | 137 | def main(): | |
| 138 | 138 | try: | |
| 139 | - from setuptools import setup, Extension | ||
| 139 | + from setuptools import Extension, setup | ||
| 140 | 140 | except ImportError: | |
| 141 | - from distutils.core import setup, Extension | ||
| 141 | + from distutils.core import Extension, setup | ||
| 142 | 142 | ||
| 143 | 143 | cflags = list(CFLAGS) | |
| 144 | 144 | cxxflags = list(CXXFLAGS) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -14,10 +14,11 @@ | |||
| 14 | 14 | import subprocess | |
| 15 | 15 | import sys | |
| 16 | 16 | import sysconfig | |
| 17 | + import tempfile | ||
| 17 | 18 | ||
| 18 | 19 | # test.utils | |
| 19 | 20 | import setup | |
| 20 | - from utils import run_command, command_stdout | ||
| 21 | + from utils import run_command, get_output | ||
| 21 | 22 | ||
| 22 | 23 | ||
| 23 | 24 | # Windows uses MSVC compiler | |
@@ -62,26 +63,24 @@ def display_title(title): | |||
| 62 | 63 | sys.stdout.flush() | |
| 63 | 64 | ||
| 64 | 65 | ||
| 65 | - def build_ext(): | ||
| 66 | + def build_ext(build_dir): | ||
| 66 | 67 | display_title("Build test extensions") | |
| 67 | - if os.path.exists("build"): | ||
| 68 | - shutil.rmtree("build") | ||
| 69 | - cmd = [sys.executable, "setup.py", "build"] | ||
| 68 | + cmd = [sys.executable, "setup.py", "build", "--build-base", build_dir] | ||
| 70 | 69 | if VERBOSE: | |
| 71 | 70 | run_command(cmd) | |
| 72 | 71 | print() | |
| 73 | 72 | else: | |
| 74 | - exitcode, stdout = command_stdout(cmd, stderr=subprocess.STDOUT) | ||
| 73 | + exitcode, stdout = get_output(cmd) | ||
| 75 | 74 | if exitcode: | |
| 76 | 75 | print(stdout.rstrip()) | |
| 77 | 76 | sys.exit(exitcode) | |
| 78 | 77 | ||
| 79 | 78 | ||
| 80 | - def import_tests(module_name): | ||
| 79 | + def import_tests(build_dir, module_name): | ||
| 81 | 80 | pythonpath = None | |
| 82 | - for name in os.listdir("build"): | ||
| 81 | + for name in os.listdir(build_dir): | ||
| 83 | 82 | if name.startswith('lib.'): | |
| 84 | - pythonpath = os.path.join("build", name) | ||
| 83 | + pythonpath = os.path.join(build_dir, name) | ||
| 85 | 84 | ||
| 86 | 85 | if not pythonpath: | |
| 87 | 86 | raise Exception("Failed to find the build directory") | |
@@ -152,7 +151,7 @@ def python_version(): | |||
| 152 | 151 | return "%s %s (%s build)" % (python_impl, pyver, build) | |
| 153 | 152 | ||
| 154 | 153 | ||
| 155 | - def run_tests(module_name, std): | ||
| 154 | + def run_tests(build_dir, module_name, std): | ||
| 156 | 155 | lang = std.upper() if std else None | |
| 157 | 156 | if VERBOSE: | |
| 158 | 157 | print("") | |
@@ -162,7 +161,7 @@ def run_tests(module_name, std): | |||
| 162 | 161 | titlte = f"{title} ({lang})" | |
| 163 | 162 | display_title(title) | |
| 164 | 163 | ||
| 165 | - testmod = import_tests(module_name) | ||
| 164 | + testmod = import_tests(build_dir, module_name) | ||
| 166 | 165 | ||
| 167 | 166 | if VERBOSE: | |
| 168 | 167 | empty_line = False | |
@@ -219,11 +218,12 @@ def main(): | |||
| 219 | 218 | if src_dir: | |
| 220 | 219 | os.chdir(src_dir) | |
| 221 | 220 | ||
| 222 | - build_ext() | ||
| 221 | + with tempfile.TemporaryDirectory() as build_dir: | ||
| 222 | + build_ext(build_dir) | ||
| 223 | 223 | ||
| 224 | - tests = setup.C_TESTS + setup.CXX_TESTS | ||
| 225 | - for module_name, std, limited in tests: | ||
| 226 | - run_tests(module_name, std) | ||
| 224 | + tests = setup.C_TESTS + setup.CXX_TESTS | ||
| 225 | + for module_name, std, limited in tests: | ||
| 226 | + run_tests(build_dir, module_name, std) | ||
| 227 | 227 | ||
| 228 | 228 | ||
| 229 | 229 | if __name__ == "__main__": | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,37 +2,33 @@ | |||
| 2 | 2 | import sys | |
| 3 | 3 | ||
| 4 | 4 | ||
| 5 | - def run_command(cmd, **kw): | ||
| 5 | + def _run_command(cmd, **kw): | ||
| 6 | + kw['shell'] = False | ||
| 6 | 7 | if hasattr(subprocess, 'run'): | |
| 7 | 8 | proc = subprocess.run(cmd, **kw) | |
| 8 | 9 | else: | |
| 9 | - kw['shell'] = False | ||
| 10 | 10 | proc = subprocess.Popen(cmd, **kw) | |
| 11 | 11 | try: | |
| 12 | - proc.wait() | ||
| 12 | + proc.communicate() | ||
| 13 | 13 | except: | |
| 14 | 14 | proc.kill() | |
| 15 | 15 | proc.wait() | |
| 16 | 16 | raise | |
| 17 | + return proc | ||
| 17 | 18 | ||
| 19 | + | ||
| 20 | + def run_command(cmd, **kw): | ||
| 21 | + proc = _run_command(cmd, **kw) | ||
| 18 | 22 | exitcode = proc.returncode | |
| 19 | 23 | if exitcode: | |
| 20 | 24 | sys.exit(exitcode) | |
| 21 | 25 | ||
| 22 | 26 | ||
| 23 | - def command_stdout(cmd, **kw): | ||
| 24 | - kw['stdout'] = subprocess.PIPE | ||
| 27 | + def get_output(cmd, **kw): | ||
| 28 | + # Legacy for text=True | ||
| 25 | 29 | kw['universal_newlines'] = True | |
| 26 | - if hasattr(subprocess, 'run'): | ||
| 27 | - proc = subprocess.run(cmd, **kw) | ||
| 28 | - return (proc.returncode, proc.stdout) | ||
| 29 | - else: | ||
| 30 | - kw['shell'] = False | ||
| 31 | - proc = subprocess.Popen(cmd, **kw) | ||
| 32 | - try: | ||
| 33 | - stdout = proc.communicate()[0] | ||
| 34 | - except: | ||
| 35 | - proc.kill() | ||
| 36 | - proc.wait() | ||
| 37 | - raise | ||
| 38 | - return (proc.returncode, stdout) | ||
| 30 | + proc = _run_command(cmd, | ||
| 31 | + stdout=subprocess.PIPE, | ||
| 32 | + stderr=subprocess.STDOUT, | ||
| 33 | + **kw) | ||
| 34 | + return (proc.returncode, proc.stdout) | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments