Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-82054: allow test runner to split test_asyncio to execute in parallel by sharding. #103927

Merged
merged 5 commits into from
Apr 30, 2023
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
This allows the test runner to execute test_multiprocessing and test_…
…asyncio in parallel.

By sharding the individual tests in test_multiprocessing and test_asyncio we are able to reduce considerably the time it takes to run the whole test suite.
  • Loading branch information
zitterbewegung committed Apr 27, 2023
commit 557e6b74fae89d58ac8343cbc934ee91c5edad76
85 changes: 51 additions & 34 deletions Lib/test/libregrtest/runtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@

class TestResult:
def __init__(
self,
name: str,
duration_sec: float = 0.0,
xml_data: list[str] | None = None,
self,
name: str,
duration_sec: float = 0.0,
xml_data: list[str] | None = None,
) -> None:
self.name = name
self.duration_sec = duration_sec
Expand All @@ -39,12 +39,12 @@ def __str__(self) -> str:

class Failed(TestResult):
def __init__(
self,
name: str,
duration_sec: float = 0.0,
xml_data: list[str] | None = None,
errors: list[tuple[str, str]] | None = None,
failures: list[tuple[str, str]] | None = None,
self,
name: str,
duration_sec: float = 0.0,
xml_data: list[str] | None = None,
errors: list[tuple[str, str]] | None = None,
failures: list[tuple[str, str]] | None = None,
) -> None:
super().__init__(name, duration_sec=duration_sec, xml_data=xml_data)
self.errors = errors
Expand Down Expand Up @@ -128,21 +128,30 @@ def __str__(self) -> str:
# small set of tests to determine if we have a basically functioning interpreter
# (i.e. if any of these fail, then anything else is likely to follow)
STDTESTS = [
'test_grammar',
'test_opcodes',
'test_dict',
'test_builtin',
'test_exceptions',
'test_types',
'test_unittest',
'test_doctest',
'test_doctest2',
'test_support'
'test_grammar',
'test_opcodes',
'test_dict',
'test_builtin',
'test_exceptions',
'test_types',
'test_unittest',
'test_doctest',
'test_doctest2',
'test_support'
]

# set of tests that we don't want to be executed when using regrtest
NOTTESTS = set()

#If these test directories are encountered recurse into them and treat each
# test_ .py or dir as a separate test module. This can increase parallelism.
# Beware this can't generally be done for any directory with sub-tests as the
# __init__.py may do things which alter what tests are to be run.

SPLITTESTDIRS = {
"test_asyncio",
"test_compiler",
}

# Storage of uncollectable objects
FOUND_GARBAGE = []
Expand All @@ -158,16 +167,24 @@ def findtestdir(path=None):
return path or os.path.dirname(os.path.dirname(__file__)) or os.curdir


def findtests(testdir=None, stdtests=STDTESTS, nottests=NOTTESTS):
def findtests(testdir=None, stdtests=STDTESTS, nottests=NOTTESTS, splittestdirs=SPLITTESTDIRS, base_mod=""):
zitterbewegung marked this conversation as resolved.
Show resolved Hide resolved
"""Return a list of all applicable test modules."""
testdir = findtestdir(testdir)
names = os.listdir(testdir)
tests = []
others = set(stdtests) | nottests
for name in names:
mod, ext = os.path.splitext(name)
if mod[:5] == "test_" and ext in (".py", "") and mod not in others:
tests.append(mod)
if mod[:5] == "test_" and mod not in others:
if mod in splittestdirs:
subdir = os.path.join(testdir, mod)
if len(base_mod):
mod = f"{base_mod}.{mod}"
else:
mod = f"test.{mod}"
tests.extend(findtests(subdir, [], nottests, splittestdirs, mod))
elif ext in (".py", ""):
tests.append(f"{base_mod}.{mod}" if len(base_mod) else mod)
return stdtests + sorted(tests)


Expand All @@ -186,7 +203,7 @@ def _runtest(ns: Namespace, test_name: str) -> TestResult:
output_on_failure = ns.verbose3

use_timeout = (
ns.timeout is not None and threading_helper.can_start_thread
ns.timeout is not None and threading_helper.can_start_thread
)
if use_timeout:
faulthandler.dump_traceback_later(ns.timeout, exit=True)
Expand Down Expand Up @@ -217,7 +234,7 @@ def _runtest(ns: Namespace, test_name: str) -> TestResult:
print_warning.orig_stderr = stream

result = _runtest_inner(ns, test_name,
display_failure=False)
display_failure=False)
if not isinstance(result, Passed):
output = stream.getvalue()
finally:
Expand All @@ -233,13 +250,13 @@ def _runtest(ns: Namespace, test_name: str) -> TestResult:
support.verbose = ns.verbose

result = _runtest_inner(ns, test_name,
display_failure=not ns.verbose)
display_failure=not ns.verbose)

if xml_list:
import xml.etree.ElementTree as ET
result.xml_data = [
ET.tostring(x).decode('us-ascii')
for x in xml_list
ET.tostring(x).decode('us-ascii')
for x in xml_list
]

result.duration_sec = time.perf_counter() - start_time
Expand Down Expand Up @@ -267,7 +284,7 @@ def runtest(ns: Namespace, test_name: str) -> TestResult:
if not ns.pgo:
msg = traceback.format_exc()
print(f"test {test_name} crashed -- {msg}",
file=sys.stderr, flush=True)
file=sys.stderr, flush=True)
return Failed(test_name)


Expand Down Expand Up @@ -328,7 +345,7 @@ def _runtest_inner2(ns: Namespace, test_name: str) -> bool:
if gc.garbage:
support.environment_altered = True
print_warning(f"{test_name} created {len(gc.garbage)} "
f"uncollectable object(s).")
f"uncollectable object(s).")

# move the uncollectable objects somewhere,
# so we don't see them again
Expand All @@ -341,7 +358,7 @@ def _runtest_inner2(ns: Namespace, test_name: str) -> bool:


def _runtest_inner(
ns: Namespace, test_name: str, display_failure: bool = True
ns: Namespace, test_name: str, display_failure: bool = True
) -> TestResult:
# Detect environment changes, handle exceptions.

Expand Down Expand Up @@ -387,7 +404,7 @@ def _runtest_inner(
if not ns.pgo:
msg = traceback.format_exc()
print(f"test {test_name} crashed -- {msg}",
file=sys.stderr, flush=True)
file=sys.stderr, flush=True)
return UncaughtException(test_name)

if refleak:
Expand Down Expand Up @@ -415,7 +432,7 @@ def cleanup_test_droppings(test_name: str, verbose: int) -> None:
kind, nuker = "file", os.unlink
else:
raise RuntimeError(f"os.path says {name!r} exists but is neither "
f"directory nor file")
f"directory nor file")

if verbose:
print_warning(f"{test_name} left behind {kind} {name!r}")
Expand All @@ -428,4 +445,4 @@ def cleanup_test_droppings(test_name: str, verbose: int) -> None:
nuker(name)
except Exception as exc:
print_warning(f"{test_name} left behind {kind} {name!r} "
f"and it couldn't be removed: {exc}")
f"and it couldn't be removed: {exc}")