mirror of
				https://github.com/python/cpython.git
				synced 2025-11-03 23:21:29 +00:00 
			
		
		
		
	* test.bisect_cmd now exit with code 0 on success, and code 1 on failure. Before, it was the opposite. * test.bisect_cmd now runs the test worker process with -X faulthandler. * regrtest RunTests: Add create_python_cmd() and bisect_cmd() methods.
		
			
				
	
	
		
			222 lines
		
	
	
	
		
			6.9 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			222 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
 |