gh-109276: libregrtest: WASM use stdout for JSON (#109355)

On Emscripten and WASI, or if --python command line is used,
libregrtest now writes JSON into stdout, instead of using a name
file.

* Add JsonFileType.STDOUT.
* Remove JsonFileType.FILENAME.
* test.pythoninfo logs environment variables related to
  cross-compilation and running Python on Emscripten/WASI.
This commit is contained in:
Victor Stinner 2023-09-13 02:24:43 +02:00 committed by GitHub
parent 75cdd9a904
commit 715f663258
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 40 additions and 33 deletions

View file

@ -21,7 +21,7 @@ from .results import TestResults
from .runtests import RunTests, JsonFile, JsonFileType
from .single import PROGRESS_MIN_TIME
from .utils import (
StrPath, StrJSON, TestName, MS_WINDOWS, TMP_PREFIX,
StrPath, StrJSON, TestName, MS_WINDOWS,
format_duration, print_warning, count, plural)
from .worker import create_worker_process, USE_PROCESS_GROUP
@ -225,16 +225,9 @@ class WorkerThread(threading.Thread):
def create_json_file(self, stack: contextlib.ExitStack) -> tuple[JsonFile, TextIO | None]:
"""Create JSON file."""
json_file_use_filename = self.runtests.json_file_use_filename()
if json_file_use_filename:
# create an empty file to make the creation atomic
# (to prevent races with other worker threads)
prefix = TMP_PREFIX + 'json_'
json_fd, json_filename = tempfile.mkstemp(prefix=prefix)
os.close(json_fd)
stack.callback(os_helper.unlink, json_filename)
json_file = JsonFile(json_filename, JsonFileType.FILENAME)
json_file_use_stdout = self.runtests.json_file_use_stdout()
if json_file_use_stdout:
json_file = JsonFile(None, JsonFileType.STDOUT)
json_tmpfile = None
else:
json_tmpfile = tempfile.TemporaryFile('w+', encoding='utf8')
@ -300,11 +293,14 @@ class WorkerThread(threading.Thread):
f"Cannot read process stdout: {exc}", None)
def read_json(self, json_file: JsonFile, json_tmpfile: TextIO | None,
stdout: str) -> TestResult:
stdout: str) -> tuple[TestResult, str]:
try:
if json_tmpfile is not None:
json_tmpfile.seek(0)
worker_json: StrJSON = json_tmpfile.read()
elif json_file.file_type == JsonFileType.STDOUT:
stdout, _, worker_json = stdout.rpartition("\n")
stdout = stdout.rstrip()
else:
with json_file.open(encoding='utf8') as json_fp:
worker_json: StrJSON = json_fp.read()
@ -319,7 +315,7 @@ class WorkerThread(threading.Thread):
raise WorkerError(self.test_name, "empty JSON", stdout)
try:
return TestResult.from_json(worker_json)
result = TestResult.from_json(worker_json)
except Exception as exc:
# gh-101634: Catch UnicodeDecodeError if stdout cannot be
# decoded from encoding
@ -327,6 +323,8 @@ class WorkerThread(threading.Thread):
raise WorkerError(self.test_name, err_msg, stdout,
state=State.MULTIPROCESSING_ERROR)
return (result, stdout)
def _runtest(self, test_name: TestName) -> MultiprocessResult:
with contextlib.ExitStack() as stack:
stdout_file = self.create_stdout(stack)
@ -341,7 +339,7 @@ class WorkerThread(threading.Thread):
if retcode is None:
raise WorkerError(self.test_name, None, stdout, state=State.TIMEOUT)
result = self.read_json(json_file, json_tmpfile, stdout)
result, stdout = self.read_json(json_file, json_tmpfile, stdout)
if retcode != 0:
raise WorkerError(self.test_name, f"Exit code {retcode}", stdout)

View file

@ -14,13 +14,16 @@ from .utils import (
class JsonFileType:
UNIX_FD = "UNIX_FD"
WINDOWS_HANDLE = "WINDOWS_HANDLE"
FILENAME = "FILENAME"
STDOUT = "STDOUT"
@dataclasses.dataclass(slots=True, frozen=True)
class JsonFile:
# See RunTests.json_file_use_filename()
file: int | StrPath
# 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:
@ -33,9 +36,6 @@ class JsonFile:
startupinfo = subprocess.STARTUPINFO()
startupinfo.lpAttributeList = {"handle_list": [self.file]}
popen_kwargs['startupinfo'] = startupinfo
case JsonFileType.FILENAME:
# Filename: nothing to do to
pass
@contextlib.contextmanager
def inherit_subprocess(self):
@ -49,6 +49,9 @@ class JsonFile:
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
@ -123,11 +126,13 @@ class RunTests:
def from_json(worker_json: StrJSON) -> 'RunTests':
return json.loads(worker_json, object_hook=_decode_runtests)
def json_file_use_filename(self) -> bool:
# json_file type depends on the platform:
# - Unix: file descriptor (int)
# - Windows: handle (int)
# - Emscripten/WASI or if --python is used: filename (str)
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

View file

@ -7,7 +7,7 @@ from test import support
from test.support import os_helper
from .setup import setup_process, setup_test_dir
from .runtests import RunTests, JsonFile
from .runtests import RunTests, JsonFile, JsonFileType
from .single import run_single_test
from .utils import (
StrPath, StrJSON, FilterTuple,
@ -67,10 +67,6 @@ def worker_process(worker_json: StrJSON) -> NoReturn:
runtests = RunTests.from_json(worker_json)
test_name = runtests.tests[0]
match_tests: FilterTuple | None = runtests.match_tests
# json_file type depends on the platform:
# - Unix: file descriptor (int)
# - Windows: handle (int)
# - Emscripten/WASI or if --python is used: filename (str)
json_file: JsonFile = runtests.json_file
setup_test_dir(runtests.test_dir)
@ -85,6 +81,10 @@ def worker_process(worker_json: StrJSON) -> NoReturn:
result = run_single_test(test_name, runtests)
if json_file.file_type == JsonFileType.STDOUT:
print()
result.write_json_into(sys.stdout)
else:
with json_file.open('w', encoding='utf-8') as json_fp:
result.write_json_into(json_fp)

View file

@ -268,6 +268,7 @@ def collect_os(info_add):
"ARCHFLAGS",
"ARFLAGS",
"AUDIODEV",
"BUILDPYTHON",
"CC",
"CFLAGS",
"COLUMNS",
@ -320,6 +321,7 @@ def collect_os(info_add):
"VIRTUAL_ENV",
"WAYLAND_DISPLAY",
"WINDIR",
"_PYTHON_HOSTRUNNER",
"_PYTHON_HOST_PLATFORM",
"_PYTHON_PROJECT_BASE",
"_PYTHON_SYSCONFIGDATA_NAME",
@ -335,7 +337,8 @@ def collect_os(info_add):
for name, value in os.environ.items():
uname = name.upper()
if (uname in ENV_VARS
# Copy PYTHON* and LC_* variables
# Copy PYTHON* variables like PYTHONPATH
# Copy LC_* variables like LC_ALL
or uname.startswith(("PYTHON", "LC_"))
# Visual Studio: VS140COMNTOOLS
or (uname.startswith("VS") and uname.endswith("COMNTOOLS"))):
@ -500,6 +503,7 @@ def collect_sysconfig(info_add):
'CFLAGS',
'CFLAGSFORSHARED',
'CONFIG_ARGS',
'HOSTRUNNER',
'HOST_GNU_TYPE',
'MACHDEP',
'MULTIARCH',