| # System modules |
| import os |
| import sys |
| import time |
| import signal |
| |
| # LLDB Modules |
| import lldb |
| from .lldbtest import * |
| from . import lldbutil |
| from lldbsuite.test.decorators import * |
| |
| |
| @skipIfRemote |
| @skipIfWindows |
| @add_test_categories(["pexpect"]) |
| class PExpectTest(TestBase): |
| NO_DEBUG_INFO_TESTCASE = True |
| PROMPT = "(lldb) " |
| |
| # Override this value in a subclass to make the test fail faster and make |
| # debugging less tedious. |
| TIMEOUT = 60 |
| |
| def expect_prompt(self): |
| self.child.expect_exact(self.PROMPT) |
| |
| def launch( |
| self, |
| executable=None, |
| extra_args=None, |
| dimensions=None, |
| run_under=None, |
| post_spawn=None, |
| encoding=None, |
| use_colors=False, |
| ): |
| # Using a log file is incompatible with using utf-8 as the encoding. |
| logfile = ( |
| getattr(sys.stdout, "buffer", sys.stdout) |
| if (self.TraceOn() and not encoding) |
| else None |
| ) |
| |
| args = [] |
| if run_under is not None: |
| args += run_under |
| args += [lldbtest_config.lldbExec, "--no-lldbinit"] |
| if not use_colors: |
| args.append("--no-use-colors") |
| for cmd in self.setUpCommands(): |
| if "use-color false" in cmd and use_colors: |
| continue |
| args += ["-O", cmd] |
| if executable is not None: |
| args += ["--file", executable] |
| if extra_args is not None: |
| args.extend(extra_args) |
| |
| env = dict(os.environ) |
| env["TERM"] = "vt100" |
| env["HOME"] = self.getBuildDir() |
| |
| timeout = self.TIMEOUT |
| |
| import pexpect |
| |
| self.child = pexpect.spawn( |
| args[0], |
| args=args[1:], |
| logfile=logfile, |
| timeout=timeout, |
| dimensions=dimensions, |
| env=env, |
| encoding=encoding, |
| ) |
| |
| # The interval in which we poll the LLDB process if we are |
| # closing/terminating the process. |
| self.polling_interval = 0.05 |
| # Timeout for each attempt to shut down LLDB at the end |
| # of the test. Note that there can be multiple attempts |
| # to shut down LLDB. |
| self.shutdown_timeout = timeout / 10 |
| |
| if post_spawn is not None: |
| post_spawn() |
| self.expect_prompt() |
| for cmd in self.setUpCommands(): |
| if "use-color false" in cmd and use_colors: |
| continue |
| self.child.expect_exact(cmd) |
| self.expect_prompt() |
| if executable is not None: |
| self.child.expect_exact("target create") |
| self.child.expect_exact("Current executable set to") |
| self.expect_prompt() |
| |
| def expect(self, cmd, substrs=None): |
| self.assertNotIn("\n", cmd) |
| # If 'substrs' is a string then this code would just check that every |
| # character of the string is in the output. |
| assert not isinstance(substrs, str), "substrs must be a collection of strings" |
| |
| self.child.sendline(cmd) |
| if substrs is not None: |
| for s in substrs: |
| self.child.expect_exact(s) |
| self.expect_prompt() |
| |
| def tearDown(self): |
| # Ensure the child is always cleaned up, even if the test didn't call |
| # quit() explicitly or failed before reaching it. |
| if self.child is not None: |
| self.quit(gracefully=True) |
| super().tearDown() |
| |
| def _poll_until_dead(self, proc): |
| """Poll proc.isalive() for up to shutdown_timeout seconds. |
| Returns True if the process exited within that window.""" |
| deadline = time.monotonic() + self.shutdown_timeout |
| while time.monotonic() < deadline: |
| if not proc.isalive(): |
| return True |
| time.sleep(self.polling_interval) |
| return not proc.isalive() |
| |
| def _terminate_child(self, force=False): |
| """Terminate the child using signals.""" |
| proc = self.child.ptyproc |
| |
| if not proc.isalive(): |
| return True |
| try: |
| # List of signals to try, starting from a friendly |
| # SIGHUP, SIGINT to SIGKILL if all else fails. |
| signals = [signal.SIGHUP, signal.SIGINT] |
| if force: |
| signals.append(signal.SIGKILL) |
| for sig in signals: |
| proc.kill(sig) |
| if self._poll_until_dead(proc): |
| return True |
| return False |
| except OSError: |
| # This handles a TOCTOU issue where we think the process is |
| # considered as 'isalive', then is actually killed by the |
| # kernel and then we try to kill it. |
| time.sleep(self.polling_interval) |
| return not proc.isalive() |
| |
| def _close_child(self, force=False): |
| """Close the connection to the child and terminate it if needed.""" |
| |
| ptyproc = self.child.ptyproc |
| if ptyproc.closed: |
| return |
| |
| # Close the PTY master fd first. |
| try: |
| self.child.sendeof() |
| ptyproc.fileobj.close() |
| except OSError: |
| # This could happen if LLDB shut down at the same time. |
| pass |
| |
| if not force: |
| # Poll briefly for the process to respond to the EOF before |
| # escalating to signals. |
| self._poll_until_dead(ptyproc) |
| |
| if ptyproc.isalive(): |
| # Kill the child with signals. This should always succeed. |
| assert self._terminate_child(force=force) |
| |
| # Make sure the object is now marked as closed. |
| ptyproc.fd = -1 |
| ptyproc.closed = True |
| |
| def quit(self, gracefully=True): |
| if self.child is None: |
| return |
| |
| try: |
| self._close_child(force=not gracefully) |
| except OSError: |
| # This can happen if LLDB quit itself because it is running in |
| # batch mode. |
| pass |
| self.child = None |
| |
| def cursor_forward_escape_seq(self, chars_to_move): |
| """ |
| Returns the escape sequence to move the cursor forward/right |
| by a certain amount of characters. |
| """ |
| return b"\x1b\\[" + str(chars_to_move).encode("utf-8") + b"C" |