mtest: refactor _run_cmd

A large part of _run_cmd is devoted to setting up and killing the
test subprocess.  Move that to a separate function to make the
test runner logic easier to understand.
pull/7836/head
Paolo Bonzini 4 years ago
parent 8cf90e6370
commit 57b918f281
  1. 186
      mesonbuild/mtest.py

@ -665,36 +665,51 @@ class SingleTestRunner:
self.test.timeout = None self.test.timeout = None
return self._run_cmd(wrap + cmd + self.test.cmd_args + self.options.test_args) return self._run_cmd(wrap + cmd + self.test.cmd_args + self.options.test_args)
def _run_cmd(self, cmd: T.List[str]) -> TestRun: def _run_subprocess(self, args: T.List[str], *, timeout: T.Optional[int],
starttime = time.time() stdout: T.IO, stderr: T.IO,
env: T.Dict[str, str], cwd: T.Optional[str]) -> T.Tuple[T.Optional[int], bool, T.Optional[str]]:
def kill_process(p: subprocess.Popen) -> T.Optional[str]:
# Python does not provide multiplatform support for
# killing a process and all its children so we need
# to roll our own.
if is_windows():
subprocess.run(['taskkill', '/F', '/T', '/PID', str(p.pid)])
else:
if self.test.extra_paths: def _send_signal_to_process_group(pgid: int, signum: int) -> None:
self.env['PATH'] = os.pathsep.join(self.test.extra_paths + ['']) + self.env['PATH'] """ sends a signal to a process group """
winecmd = [] try:
for c in cmd: os.killpg(pgid, signum)
winecmd.append(c) except ProcessLookupError:
if os.path.basename(c).startswith('wine'): # Sometimes (e.g. with Wine) this happens.
self.env['WINEPATH'] = get_wine_shortpath( # There's nothing we can do (maybe the process
winecmd, # already died) so carry on.
['Z:' + p for p in self.test.extra_paths] + self.env.get('WINEPATH', '').split(';') pass
)
break
# If MALLOC_PERTURB_ is not set, or if it is set to an empty value, # Send a termination signal to the process group that setsid()
# (i.e., the test or the environment don't explicitly set it), set # created - giving it a chance to perform any cleanup.
# it ourselves. We do this unconditionally for regular tests _send_signal_to_process_group(p.pid, signal.SIGTERM)
# because it is extremely useful to have.
# Setting MALLOC_PERTURB_="0" will completely disable this feature.
if ('MALLOC_PERTURB_' not in self.env or not self.env['MALLOC_PERTURB_']) and not self.options.benchmark:
self.env['MALLOC_PERTURB_'] = str(random.randint(1, 255))
stdout = None # Make sure the termination signal actually kills the process
stderr = None # group, otherwise retry with a SIGKILL.
if not self.options.verbose: try:
stdout = tempfile.TemporaryFile("wb+") p.communicate(timeout=0.5)
stderr = tempfile.TemporaryFile("wb+") if self.options.split else stdout except subprocess.TimeoutExpired:
if self.test.protocol is TestProtocol.TAP and stderr is stdout: _send_signal_to_process_group(p.pid, signal.SIGKILL)
stdout = tempfile.TemporaryFile("wb+") try:
p.communicate(timeout=1)
except subprocess.TimeoutExpired:
# 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()
try:
p.communicate(timeout=1)
except subprocess.TimeoutExpired:
additional_error = 'Test process could not be killed.'
except ValueError:
additional_error = 'Could not read output. Maybe the process has redirected its stdout/stderr?'
return additional_error
# Let gdb handle ^C instead of us # Let gdb handle ^C instead of us
if self.options.gdb: if self.options.gdb:
@ -713,27 +728,14 @@ class SingleTestRunner:
# errors avoid not being able to use the terminal. # errors avoid not being able to use the terminal.
os.setsid() os.setsid()
extra_cmd = [] # type: T.List[str] p = subprocess.Popen(args,
if self.test.protocol is TestProtocol.GTEST:
gtestname = self.test.name
if self.test.workdir:
gtestname = os.path.join(self.test.workdir, self.test.name)
extra_cmd.append('--gtest_output=xml:{}.xml'.format(gtestname))
p = subprocess.Popen(cmd + extra_cmd,
stdout=stdout, stdout=stdout,
stderr=stderr, stderr=stderr,
env=self.env, env=env,
cwd=self.test.workdir, cwd=cwd,
preexec_fn=preexec_fn if not is_windows() else None) preexec_fn=preexec_fn if not is_windows() else None)
timed_out = False timed_out = False
kill_test = False kill_test = False
if self.test.timeout is None:
timeout = None
elif self.options.timeout_multiplier is not None:
timeout = self.test.timeout * self.options.timeout_multiplier
else:
timeout = self.test.timeout
try: try:
p.communicate(timeout=timeout) p.communicate(timeout=timeout)
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
@ -748,49 +750,63 @@ class SingleTestRunner:
# Let us accept ^C again # Let us accept ^C again
signal.signal(signal.SIGINT, previous_sigint_handler) signal.signal(signal.SIGINT, previous_sigint_handler)
additional_error = None
if kill_test or timed_out: if kill_test or timed_out:
# Python does not provide multiplatform support for additional_error = kill_process(p)
# killing a process and all its children so we need return p.returncode, timed_out, additional_error
# to roll our own.
if is_windows():
subprocess.run(['taskkill', '/F', '/T', '/PID', str(p.pid)])
else: else:
return p.returncode, False, None
def _send_signal_to_process_group(pgid: int, signum: int) -> None: def _run_cmd(self, cmd: T.List[str]) -> TestRun:
""" sends a signal to a process group """ starttime = time.time()
try:
os.killpg(pgid, signum)
except ProcessLookupError:
# Sometimes (e.g. with Wine) this happens.
# There's nothing we can do (maybe the process
# already died) so carry on.
pass
# Send a termination signal to the process group that setsid() if self.test.extra_paths:
# created - giving it a chance to perform any cleanup. self.env['PATH'] = os.pathsep.join(self.test.extra_paths + ['']) + self.env['PATH']
_send_signal_to_process_group(p.pid, signal.SIGTERM) winecmd = []
for c in cmd:
winecmd.append(c)
if os.path.basename(c).startswith('wine'):
self.env['WINEPATH'] = get_wine_shortpath(
winecmd,
['Z:' + p for p in self.test.extra_paths] + self.env.get('WINEPATH', '').split(';')
)
break
# Make sure the termination signal actually kills the process # If MALLOC_PERTURB_ is not set, or if it is set to an empty value,
# group, otherwise retry with a SIGKILL. # (i.e., the test or the environment don't explicitly set it), set
try: # it ourselves. We do this unconditionally for regular tests
p.communicate(timeout=0.5) # because it is extremely useful to have.
except subprocess.TimeoutExpired: # Setting MALLOC_PERTURB_="0" will completely disable this feature.
_send_signal_to_process_group(p.pid, signal.SIGKILL) if ('MALLOC_PERTURB_' not in self.env or not self.env['MALLOC_PERTURB_']) and not self.options.benchmark:
try: self.env['MALLOC_PERTURB_'] = str(random.randint(1, 255))
p.communicate(timeout=1)
except subprocess.TimeoutExpired: stdout = None
# An earlier kill attempt has not worked for whatever reason. stderr = None
# Try to kill it one last time with a direct call. if not self.options.verbose:
# If the process has spawned children, they will remain around. stdout = tempfile.TemporaryFile("wb+")
p.kill() stderr = tempfile.TemporaryFile("wb+") if self.options.split else stdout
try: if self.test.protocol is TestProtocol.TAP and stderr is stdout:
p.communicate(timeout=1) stdout = tempfile.TemporaryFile("wb+")
except subprocess.TimeoutExpired:
additional_error = 'Test process could not be killed.' extra_cmd = [] # type: T.List[str]
except ValueError: if self.test.protocol is TestProtocol.GTEST:
additional_error = 'Could not read output. Maybe the process has redirected its stdout/stderr?' gtestname = self.test.name
if self.test.workdir:
gtestname = os.path.join(self.test.workdir, self.test.name)
extra_cmd.append('--gtest_output=xml:{}.xml'.format(gtestname))
if self.test.timeout is None:
timeout = None
elif self.options.timeout_multiplier is not None:
timeout = self.test.timeout * self.options.timeout_multiplier
else:
timeout = self.test.timeout
returncode, timed_out, additional_error = self._run_subprocess(cmd + extra_cmd,
timeout=timeout,
stdout=stdout,
stderr=stderr,
env=self.env,
cwd=self.test.workdir)
endtime = time.time() endtime = time.time()
duration = endtime - starttime duration = endtime - starttime
if additional_error is None: if additional_error is None:
@ -808,16 +824,16 @@ class SingleTestRunner:
stdo = "" stdo = ""
stde = additional_error stde = additional_error
if timed_out: if timed_out:
return TestRun(self.test, self.test_env, TestResult.TIMEOUT, [], p.returncode, starttime, duration, stdo, stde, cmd) return TestRun(self.test, self.test_env, TestResult.TIMEOUT, [], returncode, starttime, duration, stdo, stde, cmd)
else: else:
if self.test.protocol is TestProtocol.EXITCODE: if self.test.protocol is TestProtocol.EXITCODE:
return TestRun.make_exitcode(self.test, self.test_env, p.returncode, starttime, duration, stdo, stde, cmd) return TestRun.make_exitcode(self.test, self.test_env, returncode, starttime, duration, stdo, stde, cmd)
elif self.test.protocol is TestProtocol.GTEST: elif self.test.protocol is TestProtocol.GTEST:
return TestRun.make_gtest(self.test, self.test_env, p.returncode, starttime, duration, stdo, stde, cmd) return TestRun.make_gtest(self.test, self.test_env, returncode, starttime, duration, stdo, stde, cmd)
else: else:
if self.options.verbose: if self.options.verbose:
print(stdo, end='') print(stdo, end='')
return TestRun.make_tap(self.test, self.test_env, p.returncode, starttime, duration, stdo, stde, cmd) return TestRun.make_tap(self.test, self.test_env, returncode, starttime, duration, stdo, stde, cmd)
class TestHarness: class TestHarness:

Loading…
Cancel
Save