mirror of https://github.com/python/cpython
223 lines
6.9 KiB
Python
223 lines
6.9 KiB
Python
import contextlib
|
|
import dataclasses
|
|
import json
|
|
import os
|
|
import shlex
|
|
import subprocess
|
|
import sys
|
|
from typing import Any
|
|
|
|
from test import support
|
|
|
|
from .utils import (
|
|
StrPath, StrJSON, TestTuple, TestFilter, FilterTuple, FilterDict)
|
|
|
|
|
|
class JsonFileType:
|
|
UNIX_FD = "UNIX_FD"
|
|
WINDOWS_HANDLE = "WINDOWS_HANDLE"
|
|
STDOUT = "STDOUT"
|
|
|
|
|
|
@dataclasses.dataclass(slots=True, frozen=True)
|
|
class JsonFile:
|
|
# file type depends on file_type:
|
|
# - UNIX_FD: file descriptor (int)
|
|
# - WINDOWS_HANDLE: handle (int)
|
|
# - STDOUT: use process stdout (None)
|
|
file: int | None
|
|
file_type: str
|
|
|
|
def configure_subprocess(self, popen_kwargs: dict) -> None:
|
|
match self.file_type:
|
|
case JsonFileType.UNIX_FD:
|
|
# Unix file descriptor
|
|
popen_kwargs['pass_fds'] = [self.file]
|
|
case JsonFileType.WINDOWS_HANDLE:
|
|
# Windows handle
|
|
# We run mypy with `--platform=linux` so it complains about this:
|
|
startupinfo = subprocess.STARTUPINFO() # type: ignore[attr-defined]
|
|
startupinfo.lpAttributeList = {"handle_list": [self.file]}
|
|
popen_kwargs['startupinfo'] = startupinfo
|
|
|
|
@contextlib.contextmanager
|
|
def inherit_subprocess(self):
|
|
if self.file_type == JsonFileType.WINDOWS_HANDLE:
|
|
os.set_handle_inheritable(self.file, True)
|
|
try:
|
|
yield
|
|
finally:
|
|
os.set_handle_inheritable(self.file, False)
|
|
else:
|
|
yield
|
|
|
|
def open(self, mode='r', *, encoding):
|
|
if self.file_type == JsonFileType.STDOUT:
|
|
raise ValueError("for STDOUT file type, just use sys.stdout")
|
|
|
|
file = self.file
|
|
if self.file_type == JsonFileType.WINDOWS_HANDLE:
|
|
import msvcrt
|
|
# Create a file descriptor from the handle
|
|
file = msvcrt.open_osfhandle(file, os.O_WRONLY)
|
|
return open(file, mode, encoding=encoding)
|
|
|
|
|
|
@dataclasses.dataclass(slots=True, frozen=True)
|
|
class HuntRefleak:
|
|
warmups: int
|
|
runs: int
|
|
filename: StrPath
|
|
|
|
def bisect_cmd_args(self) -> list[str]:
|
|
# Ignore filename since it can contain colon (":"),
|
|
# and usually it's not used. Use the default filename.
|
|
return ["-R", f"{self.warmups}:{self.runs}:"]
|
|
|
|
|
|
@dataclasses.dataclass(slots=True, frozen=True)
|
|
class RunTests:
|
|
tests: TestTuple
|
|
fail_fast: bool
|
|
fail_env_changed: bool
|
|
match_tests: TestFilter
|
|
match_tests_dict: FilterDict | None
|
|
rerun: bool
|
|
forever: bool
|
|
pgo: bool
|
|
pgo_extended: bool
|
|
output_on_failure: bool
|
|
timeout: float | None
|
|
verbose: int
|
|
quiet: bool
|
|
hunt_refleak: HuntRefleak | None
|
|
test_dir: StrPath | None
|
|
use_junit: bool
|
|
coverage: bool
|
|
memory_limit: str | None
|
|
gc_threshold: int | None
|
|
use_resources: tuple[str, ...]
|
|
python_cmd: tuple[str, ...] | None
|
|
randomize: bool
|
|
random_seed: int | str
|
|
|
|
def copy(self, **override) -> 'RunTests':
|
|
state = dataclasses.asdict(self)
|
|
state.update(override)
|
|
return RunTests(**state)
|
|
|
|
def create_worker_runtests(self, **override):
|
|
state = dataclasses.asdict(self)
|
|
state.update(override)
|
|
return WorkerRunTests(**state)
|
|
|
|
def get_match_tests(self, test_name) -> FilterTuple | None:
|
|
if self.match_tests_dict is not None:
|
|
return self.match_tests_dict.get(test_name, None)
|
|
else:
|
|
return None
|
|
|
|
def get_jobs(self):
|
|
# Number of run_single_test() calls needed to run all tests.
|
|
# None means that there is not bound limit (--forever option).
|
|
if self.forever:
|
|
return None
|
|
return len(self.tests)
|
|
|
|
def iter_tests(self):
|
|
if self.forever:
|
|
while True:
|
|
yield from self.tests
|
|
else:
|
|
yield from self.tests
|
|
|
|
def json_file_use_stdout(self) -> bool:
|
|
# Use STDOUT in two cases:
|
|
#
|
|
# - If --python command line option is used;
|
|
# - On Emscripten and WASI.
|
|
#
|
|
# On other platforms, UNIX_FD or WINDOWS_HANDLE can be used.
|
|
return (
|
|
bool(self.python_cmd)
|
|
or support.is_emscripten
|
|
or support.is_wasi
|
|
)
|
|
|
|
def create_python_cmd(self) -> list[str]:
|
|
python_opts = support.args_from_interpreter_flags()
|
|
if self.python_cmd is not None:
|
|
executable = self.python_cmd
|
|
# Remove -E option, since --python=COMMAND can set PYTHON
|
|
# environment variables, such as PYTHONPATH, in the worker
|
|
# process.
|
|
python_opts = [opt for opt in python_opts if opt != "-E"]
|
|
else:
|
|
executable = (sys.executable,)
|
|
cmd = [*executable, *python_opts]
|
|
if '-u' not in python_opts:
|
|
cmd.append('-u') # Unbuffered stdout and stderr
|
|
if self.coverage:
|
|
cmd.append("-Xpresite=test.cov")
|
|
return cmd
|
|
|
|
def bisect_cmd_args(self) -> list[str]:
|
|
args = []
|
|
if self.fail_fast:
|
|
args.append("--failfast")
|
|
if self.fail_env_changed:
|
|
args.append("--fail-env-changed")
|
|
if self.timeout:
|
|
args.append(f"--timeout={self.timeout}")
|
|
if self.hunt_refleak is not None:
|
|
args.extend(self.hunt_refleak.bisect_cmd_args())
|
|
if self.test_dir:
|
|
args.extend(("--testdir", self.test_dir))
|
|
if self.memory_limit:
|
|
args.extend(("--memlimit", self.memory_limit))
|
|
if self.gc_threshold:
|
|
args.append(f"--threshold={self.gc_threshold}")
|
|
if self.use_resources:
|
|
args.extend(("-u", ','.join(self.use_resources)))
|
|
if self.python_cmd:
|
|
cmd = shlex.join(self.python_cmd)
|
|
args.extend(("--python", cmd))
|
|
if self.randomize:
|
|
args.append(f"--randomize")
|
|
args.append(f"--randseed={self.random_seed}")
|
|
return args
|
|
|
|
|
|
@dataclasses.dataclass(slots=True, frozen=True)
|
|
class WorkerRunTests(RunTests):
|
|
json_file: JsonFile
|
|
|
|
def as_json(self) -> StrJSON:
|
|
return json.dumps(self, cls=_EncodeRunTests)
|
|
|
|
@staticmethod
|
|
def from_json(worker_json: StrJSON) -> 'WorkerRunTests':
|
|
return json.loads(worker_json, object_hook=_decode_runtests)
|
|
|
|
|
|
class _EncodeRunTests(json.JSONEncoder):
|
|
def default(self, o: Any) -> dict[str, Any]:
|
|
if isinstance(o, WorkerRunTests):
|
|
result = dataclasses.asdict(o)
|
|
result["__runtests__"] = True
|
|
return result
|
|
else:
|
|
return super().default(o)
|
|
|
|
|
|
def _decode_runtests(data: dict[str, Any]) -> RunTests | dict[str, Any]:
|
|
if "__runtests__" in data:
|
|
data.pop('__runtests__')
|
|
if data['hunt_refleak']:
|
|
data['hunt_refleak'] = HuntRefleak(**data['hunt_refleak'])
|
|
if data['json_file']:
|
|
data['json_file'] = JsonFile(**data['json_file'])
|
|
return WorkerRunTests(**data)
|
|
else:
|
|
return data
|