From 07ce662bd212246e20d85de1e4f3d537565449d1 Mon Sep 17 00:00:00 2001 From: Sean Wilson Date: Thu, 3 Aug 2017 11:28:49 -0500 Subject: tests,ext: Add a new testing library proposal The new test library is split into two parts: The framework which resides in ext/, and the gem5 helping components in /tests/gem5. Change-Id: Ib4f3ae8d7eb96a7306335a3e739b7e8041aa99b9 Signed-off-by: Sean Wilson Reviewed-on: https://gem5-review.googlesource.com/4421 Reviewed-by: Giacomo Travaglini Maintainer: Jason Lowe-Power --- ext/testlib/sandbox.py | 193 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 193 insertions(+) create mode 100644 ext/testlib/sandbox.py (limited to 'ext/testlib/sandbox.py') diff --git a/ext/testlib/sandbox.py b/ext/testlib/sandbox.py new file mode 100644 index 000000000..7f8fe2d3b --- /dev/null +++ b/ext/testlib/sandbox.py @@ -0,0 +1,193 @@ +# Copyright (c) 2017 Mark D. Hill and David A. Wood +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer; +# redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution; +# neither the name of the copyright holders nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# Authors: Sean Wilson + +import multiprocessing +import pdb +import os +import sys +import threading +import traceback + +import log + +pdb._Pdb = pdb.Pdb +class ForkedPdb(pdb._Pdb): + ''' + A Pdb subclass that may be used from a forked multiprocessing child + ''' + io_manager = None + def interaction(self, *args, **kwargs): + _stdin = sys.stdin + self.io_manager.restore_pipes() + try: + sys.stdin = open('/dev/stdin') + pdb._Pdb.interaction(self, *args, **kwargs) + finally: + sys.stdin = _stdin + self.io_manager.replace_pipes() + + +#TODO Refactor duplicate stdout, stderr logic +class IoManager(object): + def __init__(self, test, suite): + self.test = test + self.suite = suite + self.log = log.test_log + self._init_pipes() + + def _init_pipes(self): + self.stdout_rp, self.stdout_wp = os.pipe() + self.stderr_rp, self.stderr_wp = os.pipe() + + def close_parent_pipes(self): + os.close(self.stdout_wp) + os.close(self.stderr_wp) + + def setup(self): + self.replace_pipes() + self.fixup_pdb() + + def fixup_pdb(self): + ForkedPdb.io_manager = self + pdb.Pdb = ForkedPdb + + def replace_pipes(self): + self.old_stderr = os.dup(sys.stderr.fileno()) + self.old_stdout = os.dup(sys.stdout.fileno()) + + os.dup2(self.stderr_wp, sys.stderr.fileno()) + sys.stderr = os.fdopen(self.stderr_wp, 'w', 0) + os.dup2(self.stdout_wp, sys.stdout.fileno()) + sys.stdout = os.fdopen(self.stdout_wp, 'w', 0) + + def restore_pipes(self): + self.stderr_wp = os.dup(sys.stderr.fileno()) + self.stdout_wp = os.dup(sys.stdout.fileno()) + + os.dup2(self.old_stderr, sys.stderr.fileno()) + sys.stderr = os.fdopen(self.old_stderr, 'w', 0) + os.dup2(self.old_stdout, sys.stdout.fileno()) + sys.stdout = os.fdopen(self.old_stdout, 'w', 0) + + def start_loggers(self): + self.log_ouput() + + def log_ouput(self): + def _log_output(pipe, log_callback): + with os.fdopen(pipe, 'r') as pipe: + # Read iteractively, don't allow input to fill the pipe. + for line in iter(pipe.readline, ''): + log_callback(line) + + # Don't keep a backpointer to self in the thread. + log = self.log + test = self.test + suite = self.suite + + self.stdout_thread = threading.Thread( + target=_log_output, + args=(self.stdout_rp, + lambda buf: log.test_stdout(test, suite, buf)) + ) + self.stderr_thread = threading.Thread( + target=_log_output, + args=(self.stderr_rp, + lambda buf: log.test_stderr(test, suite, buf)) + ) + + # Daemon + Join to not lock up main thread if something breaks + # but provide consistent execution if nothing goes wrong. + self.stdout_thread.daemon = True + self.stderr_thread.daemon = True + self.stdout_thread.start() + self.stderr_thread.start() + + def join_loggers(self): + self.stdout_thread.join() + self.stderr_thread.join() + + +class SubprocessException(Exception): + def __init__(self, exception, trace): + super(SubprocessException, self).__init__(trace) + +class ExceptionProcess(multiprocessing.Process): + class Status(): + def __init__(self, exitcode, exception_tuple): + self.exitcode = exitcode + if exception_tuple is not None: + self.trace = exception_tuple[1] + self.exception = exception_tuple[0] + else: + self.exception = None + self.trace = None + + def __init__(self, *args, **kwargs): + multiprocessing.Process.__init__(self, *args, **kwargs) + self._pconn, self._cconn = multiprocessing.Pipe() + self._exception = None + + def run(self): + try: + super(ExceptionProcess, self).run() + self._cconn.send(None) + except Exception as e: + tb = traceback.format_exc() + self._cconn.send((e, tb)) + raise + + @property + def status(self): + if self._pconn.poll(): + self._exception = self._pconn.recv() + + return self.Status(self.exitcode, self._exception) + + +class Sandbox(object): + def __init__(self, test_parameters): + + self.params = test_parameters + self.io_manager = IoManager(self.params.test, self.params.suite) + + self.p = ExceptionProcess(target=self.entrypoint) + # Daemon + Join to not lock up main thread if something breaks + self.p.daemon = True + self.io_manager.start_loggers() + self.p.start() + self.io_manager.close_parent_pipes() + self.p.join() + self.io_manager.join_loggers() + + status = self.p.status + if status.exitcode: + raise SubprocessException(status.exception, status.trace) + + def entrypoint(self): + self.io_manager.setup() + self.params.test.test(self.params) -- cgit v1.2.3