| 
									
										
										
										
											2023-09-13 00:41:25 +02:00
										 |  |  | import contextlib | 
					
						
							| 
									
										
										
										
											2023-09-11 02:07:18 +02:00
										 |  |  | import dataclasses | 
					
						
							|  |  |  | import json | 
					
						
							| 
									
										
										
										
											2023-09-13 00:41:25 +02:00
										 |  |  | import os | 
					
						
							| 
									
										
										
										
											2024-02-18 21:06:39 +01:00
										 |  |  | import shlex | 
					
						
							| 
									
										
										
										
											2023-09-13 00:41:25 +02:00
										 |  |  | import subprocess | 
					
						
							| 
									
										
										
										
											2024-02-18 21:06:39 +01:00
										 |  |  | import sys | 
					
						
							| 
									
										
										
										
											2023-09-11 02:07:18 +02:00
										 |  |  | from typing import Any | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-13 00:41:25 +02:00
										 |  |  | from test import support | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-11 09:02:35 +02:00
										 |  |  | from .utils import ( | 
					
						
							| 
									
										
										
										
											2023-10-21 17:44:46 +03:00
										 |  |  |     StrPath, StrJSON, TestTuple, TestFilter, FilterTuple, FilterDict) | 
					
						
							| 
									
										
										
										
											2023-09-11 02:07:18 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-13 00:41:25 +02:00
										 |  |  | class JsonFileType: | 
					
						
							|  |  |  |     UNIX_FD = "UNIX_FD" | 
					
						
							|  |  |  |     WINDOWS_HANDLE = "WINDOWS_HANDLE" | 
					
						
							| 
									
										
										
										
											2023-09-13 02:24:43 +02:00
										 |  |  |     STDOUT = "STDOUT" | 
					
						
							| 
									
										
										
										
											2023-09-13 00:41:25 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @dataclasses.dataclass(slots=True, frozen=True) | 
					
						
							|  |  |  | class JsonFile: | 
					
						
							| 
									
										
										
										
											2023-09-13 02:24:43 +02:00
										 |  |  |     # file type depends on file_type: | 
					
						
							|  |  |  |     # - UNIX_FD: file descriptor (int) | 
					
						
							|  |  |  |     # - WINDOWS_HANDLE: handle (int) | 
					
						
							|  |  |  |     # - STDOUT: use process stdout (None) | 
					
						
							|  |  |  |     file: int | None | 
					
						
							| 
									
										
										
										
											2023-09-13 00:41:25 +02:00
										 |  |  |     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 | 
					
						
							| 
									
										
										
										
											2023-11-30 23:00:14 +00:00
										 |  |  |                 # We run mypy with `--platform=linux` so it complains about this: | 
					
						
							|  |  |  |                 startupinfo = subprocess.STARTUPINFO()  # type: ignore[attr-defined] | 
					
						
							| 
									
										
										
										
											2023-09-13 00:41:25 +02:00
										 |  |  |                 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): | 
					
						
							| 
									
										
										
										
											2023-09-13 02:24:43 +02:00
										 |  |  |         if self.file_type == JsonFileType.STDOUT: | 
					
						
							|  |  |  |             raise ValueError("for STDOUT file type, just use sys.stdout") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-13 00:41:25 +02:00
										 |  |  |         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) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-11 02:07:18 +02:00
										 |  |  | @dataclasses.dataclass(slots=True, frozen=True) | 
					
						
							|  |  |  | class HuntRefleak: | 
					
						
							|  |  |  |     warmups: int | 
					
						
							|  |  |  |     runs: int | 
					
						
							|  |  |  |     filename: StrPath | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-18 21:06:39 +01:00
										 |  |  |     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}:"] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-11 02:07:18 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | @dataclasses.dataclass(slots=True, frozen=True) | 
					
						
							|  |  |  | class RunTests: | 
					
						
							|  |  |  |     tests: TestTuple | 
					
						
							| 
									
										
										
										
											2023-09-12 05:35:08 +02:00
										 |  |  |     fail_fast: bool | 
					
						
							|  |  |  |     fail_env_changed: bool | 
					
						
							| 
									
										
										
										
											2023-10-21 17:44:46 +03:00
										 |  |  |     match_tests: TestFilter | 
					
						
							| 
									
										
										
										
											2023-09-12 05:35:08 +02:00
										 |  |  |     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 | 
					
						
							| 
									
										
										
										
											2023-11-10 18:17:45 +01:00
										 |  |  |     coverage: bool | 
					
						
							| 
									
										
										
										
											2023-09-12 05:35:08 +02:00
										 |  |  |     memory_limit: str | None | 
					
						
							|  |  |  |     gc_threshold: int | None | 
					
						
							| 
									
										
										
										
											2023-09-14 19:33:18 +01:00
										 |  |  |     use_resources: tuple[str, ...] | 
					
						
							|  |  |  |     python_cmd: tuple[str, ...] | None | 
					
						
							| 
									
										
										
										
											2023-09-12 05:35:08 +02:00
										 |  |  |     randomize: bool | 
					
						
							| 
									
										
										
										
											2023-10-21 10:37:48 +02:00
										 |  |  |     random_seed: int | str | 
					
						
							| 
									
										
										
										
											2023-09-11 02:07:18 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-01 14:46:50 +01:00
										 |  |  |     def copy(self, **override) -> 'RunTests': | 
					
						
							| 
									
										
										
										
											2023-09-11 02:07:18 +02:00
										 |  |  |         state = dataclasses.asdict(self) | 
					
						
							|  |  |  |         state.update(override) | 
					
						
							|  |  |  |         return RunTests(**state) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-01 14:46:50 +01:00
										 |  |  |     def create_worker_runtests(self, **override): | 
					
						
							|  |  |  |         state = dataclasses.asdict(self) | 
					
						
							|  |  |  |         state.update(override) | 
					
						
							|  |  |  |         return WorkerRunTests(**state) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-11 02:07:18 +02:00
										 |  |  |     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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-12 05:47:04 +02:00
										 |  |  |     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) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-11 02:07:18 +02:00
										 |  |  |     def iter_tests(self): | 
					
						
							|  |  |  |         if self.forever: | 
					
						
							|  |  |  |             while True: | 
					
						
							|  |  |  |                 yield from self.tests | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             yield from self.tests | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-13 02:24:43 +02:00
										 |  |  |     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. | 
					
						
							| 
									
										
										
										
											2023-09-13 00:41:25 +02:00
										 |  |  |         return ( | 
					
						
							|  |  |  |             bool(self.python_cmd) | 
					
						
							|  |  |  |             or support.is_emscripten | 
					
						
							|  |  |  |             or support.is_wasi | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-02-18 21:06:39 +01:00
										 |  |  |     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 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-11 02:07:18 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-01 14:46:50 +01:00
										 |  |  | @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) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-09-11 02:07:18 +02:00
										 |  |  | class _EncodeRunTests(json.JSONEncoder): | 
					
						
							|  |  |  |     def default(self, o: Any) -> dict[str, Any]: | 
					
						
							| 
									
										
										
										
											2023-12-01 14:46:50 +01:00
										 |  |  |         if isinstance(o, WorkerRunTests): | 
					
						
							| 
									
										
										
										
											2023-09-11 02:07:18 +02:00
										 |  |  |             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']) | 
					
						
							| 
									
										
										
										
											2023-09-13 00:41:25 +02:00
										 |  |  |         if data['json_file']: | 
					
						
							|  |  |  |             data['json_file'] = JsonFile(**data['json_file']) | 
					
						
							| 
									
										
										
										
											2023-12-01 14:46:50 +01:00
										 |  |  |         return WorkerRunTests(**data) | 
					
						
							| 
									
										
										
										
											2023-09-11 02:07:18 +02:00
										 |  |  |     else: | 
					
						
							|  |  |  |         return data |