Make it easier to interrupt testrunner.py with Ctrl-C.
Ctrl-C now kills the ongoing tests and cancels the remaining ones.
Test: art/test/testrunner/testrunner.py --target --64 --optimizing -j4
Bug: 186530369
Change-Id: I5dbd6932529a4902f4f03e5a36fe3aa8602ea4a6
diff --git a/test/testrunner/testrunner.py b/test/testrunner/testrunner.py
index 89cfc30..bdd0d10 100755
--- a/test/testrunner/testrunner.py
+++ b/test/testrunner/testrunner.py
@@ -75,6 +75,7 @@
import subprocess
import sys
import tempfile
+import threading
import time
import env
@@ -153,6 +154,47 @@
# value: set of variants user wants to run of type <key>.
_user_input_variants = collections.defaultdict(set)
+
+class ChildProcessTracker(object):
+ """Keeps track of forked child processes to be able to kill them."""
+
+ def __init__(self):
+ self.procs = {} # dict from pid to subprocess.Popen object
+ self.mutex = threading.Lock()
+
+ def wait(self, proc, timeout):
+ """Waits on the given subprocess and makes it available to kill_all meanwhile.
+
+ Args:
+ proc: The subprocess.Popen object to wait on.
+ timeout: Timeout passed on to proc.communicate.
+
+ Returns: A tuple of the process stdout output and its return value.
+ """
+ with self.mutex:
+ if self.procs is not None:
+ self.procs[proc.pid] = proc
+ else:
+ os.killpg(proc.pid, signal.SIGKILL) # kill_all has already been called.
+ try:
+ output = proc.communicate(timeout=timeout)[0]
+ return_value = proc.wait()
+ return output, return_value
+ finally:
+ with self.mutex:
+ if self.procs is not None:
+ del self.procs[proc.pid]
+
+ def kill_all(self):
+ """Kills all currently running processes and any future ones."""
+ with self.mutex:
+ for pid in self.procs:
+ os.killpg(pid, signal.SIGKILL)
+ self.procs = None # Make future wait() calls kill their processes immediately.
+
+child_process_tracker = ChildProcessTracker()
+
+
def setup_csv_result():
"""Set up the CSV output if required."""
global csv_writer
@@ -545,15 +587,20 @@
test_futures.append(
start_combination(executor, config_tuple, options_all, "")) # no address size
- tests_done = 0
- for test_future in concurrent.futures.as_completed(test_futures):
- (test, status, failure_info, test_time) = test_future.result()
- tests_done += 1
- print_test_info(tests_done, test, status, failure_info, test_time)
- if failure_info and not env.ART_TEST_KEEP_GOING:
- for f in test_futures:
- f.cancel()
- break
+ try:
+ tests_done = 0
+ for test_future in concurrent.futures.as_completed(test_futures):
+ (test, status, failure_info, test_time) = test_future.result()
+ tests_done += 1
+ print_test_info(tests_done, test, status, failure_info, test_time)
+ if failure_info and not env.ART_TEST_KEEP_GOING:
+ for f in test_futures:
+ f.cancel()
+ break
+ except KeyboardInterrupt:
+ for f in test_futures:
+ f.cancel()
+ child_process_tracker.kill_all()
executor.shutdown(True)
@contextlib.contextmanager
@@ -620,8 +667,8 @@
universal_newlines=True,
start_new_session=True,
)
- script_output = proc.communicate(timeout=timeout)[0]
- test_passed = not proc.wait()
+ script_output, return_value = child_process_tracker.wait(proc, timeout)
+ test_passed = not return_value
test_time_seconds = time.monotonic() - test_start_time
test_time = datetime.timedelta(seconds=test_time_seconds)