diff --git a/avocado/gdb.py b/avocado/gdb.py index cd243049da463b32a419d2799fb02d96ae2fe31b..60413f6fc0ffaed27c4e2ba41d46d0f20a3f6778 100644 --- a/avocado/gdb.py +++ b/avocado/gdb.py @@ -19,6 +19,7 @@ Module that provides communication with GDB via its GDB/MI interpreter import os import time import fcntl +import tempfile import subprocess from avocado.utils import network @@ -451,12 +452,18 @@ class GDBServer(object): def __init__(self): self.port = network.find_free_port(*self.PORT_RANGE) + prefix = 'avocado_gdbserver_%s_' % self.port + _, self.stdout_path = tempfile.mkstemp(prefix=prefix + 'stdout_') + self.stdout = open(self.stdout_path, 'w') + _, self.stderr_path = tempfile.mkstemp(prefix=prefix + 'stderr_') + self.stderr = open(self.stderr_path, 'w') + args = self.ARGS[:] args.append(":%s" % self.port) self.process = subprocess.Popen(args, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + stdout=self.stdout, + stderr=self.stderr, close_fds=True) def exit(self, force=True): @@ -488,3 +495,5 @@ class GDBServer(object): temp_client.process.kill() temp_client.process.wait() self.process.wait() + self.stdout.close() + self.stderr.close() diff --git a/avocado/utils/process.py b/avocado/utils/process.py index 162dcdb7bddf151987ff439b76a04c62e6cf52bc..20373648c71b27dc02e1026a2c52bd058db1e1c4 100644 --- a/avocado/utils/process.py +++ b/avocado/utils/process.py @@ -452,7 +452,27 @@ class GDBSubProcess(object): Runs a subprocess inside the GNU Debugger ''' - def __init__(self, cmd, verbose=True, record_stream_files=False): + def __init__(self, cmd, verbose=True, allow_output_check='all'): + """ + Creates the subprocess object, stdout/err, reader threads and locks. + + :param cmd: Command line to run. + :type cmd: str + :param verbose: Whether to log the command run and stdout/stderr. + Currently unused and provided for compatibility only. + :type verbose: bool + :param allow_output_check: Whether to log the command stream outputs + (stdout and stderr) in the test stream + files. Valid values: 'stdout', for + allowing only standard output, 'stderr', + to allow only standard error, 'all', + to allow both standard output and error + (default), and 'none', to allow + none to be recorded. Currently unused and + provided for compatibility only. + :type allow_output_check: str + """ + self.cmd = cmd self.args = shlex.split(cmd) @@ -658,10 +678,31 @@ class GDBSubProcess(object): self.gdb.set_break(b, ignore_error=True) result = self.gdb.run(self.args[1:]) + + # Collect gdbserver stdout and stderr file information for debugging + # based on its process ID and stream (stdout or stderr) + current_test = runtime.CURRENT_TEST + if current_test is not None: + stdout_name = 'gdbserver.%s.stdout' % self.gdb_server.process.pid + stdout_path = os.path.join(current_test.logdir, stdout_name) + stderr_name = 'gdbserver.%s.stderr' % self.gdb_server.process.pid + stderr_path = os.path.join(current_test.logdir, stderr_name) + while True: r = self.wait_for_exit() if r: self.gdb.disconnect() + + # Now collect the gdbserver stdout and stderr file themselves + # and populate the CommandResult stdout and stderr + if current_test is not None: + if os.path.exists(self.gdb_server.stdout_path): + shutil.copy(self.gdb_server.stdout_path, stdout_path) + self.result.stdout = open(stdout_path, 'r').read() + if os.path.exists(self.gdb_server.stderr_path): + shutil.copy(self.gdb_server.stderr_path, stderr_path) + self.result.stderr = open(stderr_path, 'r').read() + self.gdb_server.exit() return self.result diff --git a/docs/source/DebuggingWithGDB.rst b/docs/source/DebuggingWithGDB.rst index 73e64ce44065f3402ea3a92c43a0e01525837693..15c1269f05dcfae974fd1fa2893934f89f7be32a 100644 --- a/docs/source/DebuggingWithGDB.rst +++ b/docs/source/DebuggingWithGDB.rst @@ -15,18 +15,48 @@ avocado man page. Caveats ------- -Currently there is one big caveat when running binaries inside GDB: there's -no way to perform input/output with the process `STDIN`, `STDOUT` and `STDERR`. +Currently, when using the Avocado GDB plugin, that is, when using the +`--gdb-run-bin` option, there are some caveats you should be aware of: -There are a couple of reasons for that: +* It is not currently compatible with Avocado's `--output-check-record` feature +* There's no way to perform proper input to the process, that is, manipulate its `STDIN` +* The process `STDERR` content is mixed with the content generated by `gdbserver` on its + own `STDERR` (because they are in fact, the same thing) -* The process that runs inside GDB has, by default, the same controlling `tty` of the `gdb` process that avocado *initially* runs. When avocado reaches a given breakpoint, it pauses the tests and allows the user to run another `gdb` process. This second `gdb` process is still connected to the same running process by means of a separate `gdbserver`. At this point, the process is still using the original `tty`. +But, you can still depend on the process `STDOUT`, as exemplified by this fictional +test:: -* Even when using a single `tty`, there's no reliable way of separating multiple streams of data, say from `gdb` and from your application, or even `STDOUT` and `STDERR` streams from either one. + from avocado import test + from avocado.utils import process -The complete resolution to this caveat suggests the creating of a Pseudo `tty` -for the process running inside GDB, so that the process is the only entity reading -and writing to that `tty`. This will be addressed in future avocado versions. + class hello_output_test(test.Test): + + def action(self): + result = process.run("/path/to/hello", ignore_status=True) + self.assertIn("hello\n", result.stdout) + +If run under GDB or not, `result.stdout` behavior and content is expected to be the same. + +Reasons for the caveats +~~~~~~~~~~~~~~~~~~~~~~~ + +There are a two basic reasons for the mentioned caveats: + +* The architecture of Avocado's GDB feature +* GDB's own behavior and limitations + +When using the Avocado GDB plugin, that is, `--gdb-run-bin`, avocado runs a `gdbserver` instance +transparently and controls it by means of a `gdb` process. When a given event happens, say a +breakpoint is reached, it disconnects its own `gdb` from the server, and allows the user to use +a standard `gdb` to connect to the `gdbserver`. This provides a natural and seamless user experience. + +But, `gdbserver` has some limitations at this point, including: + +* Not being able to set a controlling `tty` +* Not separating its own `STDERR` content from the application being run + +These limitations are being addressed both on Avocado and GDB, and will be resolved in future avocado +versions. Workaround ~~~~~~~~~~ diff --git a/examples/tests/gdbtest.py b/examples/tests/gdbtest.py index f3f85b1ffea28694af745e7bb7b8e35bc7c9267c..1abd4723ac2de991d87e30e101a56768949d3af5 100644 --- a/examples/tests/gdbtest.py +++ b/examples/tests/gdbtest.py @@ -356,6 +356,42 @@ class gdbtest(test.Test): result = process.run(cmd, ignore_status=True) self.assertEquals(result.exit_status, exp) + def test_server_stderr(self): + self.log.info('Testing server stderr collection') + s = gdb.GDBServer() + s.exit() + self.assertTrue(os.path.exists(s.stderr_path)) + + stderr_lines = open(s.stderr_path, 'r').readlines() + listening_line = "Listening on port %s\n" % s.port + self.assertIn(listening_line, stderr_lines) + + def test_server_stdout(self): + self.log.info('Testing server stdout/stderr collection') + s = gdb.GDBServer() + c = gdb.GDB() + c.connect(s.port) + c.set_file(self.return99_binary_path) + c.run() + s.exit() + + self.assertTrue(os.path.exists(s.stdout_path)) + self.assertTrue(os.path.exists(s.stderr_path)) + + stdout_lines = open(s.stdout_path, 'r').readlines() + self.assertIn("return 99\n", stdout_lines) + + def test_interactive_stdout(self): + """ + Tests avocado's GDB plugin features + + If GDB command line options are given, `--gdb-run-bin=return99` for + this particular test, the test will stop at binary main() function. + """ + self.log.info('Testing GDB interactivity') + result = process.run(self.return99_binary_path, ignore_status=True) + self.assertIn("return 99\n", result.stdout) + def action(self): """ Execute tests @@ -377,3 +413,6 @@ class gdbtest(test.Test): self.test_interactive() self.test_interactive_args() self.test_exit_status() + self.test_server_stderr() + self.test_server_stdout() + self.test_interactive_stdout()