aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaolo Bonzini <pbonzini@redhat.com>2020-11-25 13:19:55 +0100
committerPaolo Bonzini <pbonzini@redhat.com>2021-01-07 19:20:40 +0100
commit0ccc70ae1bb85095c5d7313d68bbda5ddb0d1530 (patch)
treee625956251ec60b6ef4f3c94d62ee1daed8a558c
parent63e26ba05fe9f367b8e83800ebd22ccb06204eda (diff)
downloadmeson-0ccc70ae1bb85095c5d7313d68bbda5ddb0d1530.zip
meson-0ccc70ae1bb85095c5d7313d68bbda5ddb0d1530.tar.gz
meson-0ccc70ae1bb85095c5d7313d68bbda5ddb0d1530.tar.bz2
mtest: do not wait inside _run_subprocess
We would like SingleTestRunner to run code before waiting on the process, for example starting tasks to read stdout and stderr. Return a new object that is able to complete _run_subprocess's task. In the next patch, SingleTestRunner will also use the object to get hold of the stdout and stderr StreamReaders. Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
-rw-r--r--mesonbuild/mtest.py147
1 files changed, 81 insertions, 66 deletions
diff --git a/mesonbuild/mtest.py b/mesonbuild/mtest.py
index 454646d..e2c2c56 100644
--- a/mesonbuild/mtest.py
+++ b/mesonbuild/mtest.py
@@ -924,6 +924,69 @@ async def complete_all(futures: T.Iterable[asyncio.Future]) -> None:
if not f.cancelled():
f.result()
+class TestSubprocess:
+ def __init__(self, p: asyncio.subprocess.Process, postwait_fn: T.Callable[[], None] = None):
+ self._process = p
+ self.postwait_fn = postwait_fn # type: T.Callable[[], None]
+
+ async def _kill(self) -> T.Optional[str]:
+ # Python does not provide multiplatform support for
+ # killing a process and all its children so we need
+ # to roll our own.
+ p = self._process
+ try:
+ if is_windows():
+ subprocess.run(['taskkill', '/F', '/T', '/PID', str(p.pid)])
+ else:
+ # Send a termination signal to the process group that setsid()
+ # created - giving it a chance to perform any cleanup.
+ os.killpg(p.pid, signal.SIGTERM)
+
+ # Make sure the termination signal actually kills the process
+ # group, otherwise retry with a SIGKILL.
+ await try_wait_one(p.wait(), timeout=0.5)
+ if p.returncode is not None:
+ return None
+
+ os.killpg(p.pid, signal.SIGKILL)
+
+ await try_wait_one(p.wait(), timeout=1)
+ if p.returncode is not None:
+ return None
+
+ # An earlier kill attempt has not worked for whatever reason.
+ # Try to kill it one last time with a direct call.
+ # If the process has spawned children, they will remain around.
+ p.kill()
+ await try_wait_one(p.wait(), timeout=1)
+ if p.returncode is not None:
+ return None
+ return 'Test process could not be killed.'
+ except ProcessLookupError:
+ # Sometimes (e.g. with Wine) this happens. There's nothing
+ # we can do, probably the process already died so just wait
+ # for the event loop to pick that up.
+ await p.wait()
+ return None
+
+ async def wait(self, timeout: T.Optional[int]) -> T.Tuple[int, TestResult, T.Optional[str]]:
+ p = self._process
+ result = None
+ additional_error = None
+ try:
+ await try_wait_one(p.wait(), timeout=timeout)
+ if p.returncode is None:
+ additional_error = await self._kill()
+ result = TestResult.TIMEOUT
+ except asyncio.CancelledError:
+ # The main loop must have seen Ctrl-C.
+ additional_error = await self._kill()
+ result = TestResult.INTERRUPT
+ finally:
+ if self.postwait_fn:
+ self.postwait_fn()
+
+ return p.returncode or 0, result, additional_error
class SingleTestRunner:
@@ -969,48 +1032,9 @@ class SingleTestRunner:
await self._run_cmd(wrap + cmd + self.test.cmd_args + self.options.test_args)
return self.runobj
- async def _run_subprocess(self, args: T.List[str], *, timeout: T.Optional[int],
+ async def _run_subprocess(self, args: T.List[str], *,
stdout: T.IO, stderr: T.IO,
- env: T.Dict[str, str], cwd: T.Optional[str]) -> T.Tuple[int, TestResult, T.Optional[str]]:
- async def kill_process(p: asyncio.subprocess.Process) -> T.Optional[str]:
- # Python does not provide multiplatform support for
- # killing a process and all its children so we need
- # to roll our own.
- try:
- if is_windows():
- subprocess.run(['taskkill', '/F', '/T', '/PID', str(p.pid)])
- else:
- # Send a termination signal to the process group that setsid()
- # created - giving it a chance to perform any cleanup.
- os.killpg(p.pid, signal.SIGTERM)
-
- # Make sure the termination signal actually kills the process
- # group, otherwise retry with a SIGKILL.
- await try_wait_one(p.wait(), timeout=0.5)
- if p.returncode is not None:
- return None
-
- os.killpg(p.pid, signal.SIGKILL)
-
- await try_wait_one(p.wait(), timeout=1)
- if p.returncode is not None:
- return None
-
- # An earlier kill attempt has not worked for whatever reason.
- # Try to kill it one last time with a direct call.
- # If the process has spawned children, they will remain around.
- p.kill()
- await try_wait_one(p.wait(), timeout=1)
- if p.returncode is not None:
- return None
- return 'Test process could not be killed.'
- except ProcessLookupError:
- # Sometimes (e.g. with Wine) this happens. There's nothing
- # we can do, probably the process already died so just wait
- # for the event loop to pick that up.
- await p.wait()
- return None
-
+ env: T.Dict[str, str], cwd: T.Optional[str]) -> TestSubprocess:
# Let gdb handle ^C instead of us
if self.options.gdb:
previous_sigint_handler = signal.getsignal(signal.SIGINT)
@@ -1028,31 +1052,18 @@ class SingleTestRunner:
# errors avoid not being able to use the terminal.
os.setsid()
+ def postwait_fn() -> None:
+ if self.options.gdb:
+ # Let us accept ^C again
+ signal.signal(signal.SIGINT, previous_sigint_handler)
+
p = await asyncio.create_subprocess_exec(*args,
stdout=stdout,
stderr=stderr,
env=env,
cwd=cwd,
preexec_fn=preexec_fn if not is_windows() else None)
- result = None
- additional_error = None
- try:
- await try_wait_one(p.wait(), timeout=timeout)
- if p.returncode is None:
- if self.options.verbose:
- print('{} time out (After {} seconds)'.format(self.test.name, timeout))
- additional_error = await kill_process(p)
- result = TestResult.TIMEOUT
- except asyncio.CancelledError:
- # The main loop must have seen Ctrl-C.
- additional_error = await kill_process(p)
- result = TestResult.INTERRUPT
- finally:
- if self.options.gdb:
- # Let us accept ^C again
- signal.signal(signal.SIGINT, previous_sigint_handler)
-
- return p.returncode or 0, result, additional_error
+ return TestSubprocess(p, postwait_fn=postwait_fn if not is_windows() else None)
async def _run_cmd(self, cmd: T.List[str]) -> None:
if self.test.extra_paths:
@@ -1097,12 +1108,16 @@ class SingleTestRunner:
else:
timeout = self.test.timeout
- returncode, result, additional_error = await self._run_subprocess(cmd + extra_cmd,
- timeout=timeout,
- stdout=stdout,
- stderr=stderr,
- env=self.env,
- cwd=self.test.workdir)
+ p = await self._run_subprocess(cmd + extra_cmd,
+ stdout=stdout,
+ stderr=stderr,
+ env=self.env,
+ cwd=self.test.workdir)
+
+ returncode, result, additional_error = await p.wait(timeout)
+ if result is TestResult.TIMEOUT and self.options.verbose:
+ print('{} time out (After {} seconds)'.format(self.test.name, timeout))
+
if additional_error is None:
if stdout is None:
stdo = ''