When debugging PExpect tests, the 60 second timeout can make that process rather tedious. For TestStatusline, I used a class variable to easily override it while iterating but the idea is applicable more generally.
238 lines
8.6 KiB
Python
238 lines
8.6 KiB
Python
import lldb
|
|
import os
|
|
import re
|
|
import socket
|
|
import time
|
|
|
|
from contextlib import closing
|
|
from lldbsuite.test.decorators import *
|
|
from lldbsuite.test.lldbtest import *
|
|
from lldbsuite.test.lldbpexpect import PExpectTest
|
|
from lldbgdbserverutils import get_lldb_server_exe
|
|
|
|
|
|
# PExpect uses many timeouts internally and doesn't play well
|
|
# under ASAN on a loaded machine..
|
|
@skipIfAsan
|
|
class TestStatusline(PExpectTest):
|
|
TERMINAL_HEIGHT = 10
|
|
TERMINAL_WIDTH = 60
|
|
|
|
def do_setup(self):
|
|
# Create a target and run to a breakpoint.
|
|
exe = self.getBuildArtifact("a.out")
|
|
self.expect(
|
|
"target create {}".format(exe), substrs=["Current executable set to"]
|
|
)
|
|
self.expect('breakpoint set -p "Break here"', substrs=["Breakpoint 1"])
|
|
self.expect("run", substrs=["stop reason"])
|
|
self.resize()
|
|
|
|
def resize(self, height=None, width=None):
|
|
height = self.TERMINAL_HEIGHT if not height else height
|
|
width = self.TERMINAL_WIDTH if not width else width
|
|
# Change the terminal dimensions. When we launch the tests, we reset
|
|
# all the settings, leaving the terminal dimensions unset.
|
|
self.child.setwinsize(height, width)
|
|
|
|
def test(self):
|
|
"""Basic test for the statusline."""
|
|
self.build()
|
|
self.launch()
|
|
self.do_setup()
|
|
|
|
# Enable the statusline and check for the control character and that we
|
|
# can see the target, the location and the stop reason.
|
|
self.expect('set set separator "| "')
|
|
self.expect(
|
|
"set set show-statusline true",
|
|
[
|
|
"\x1b[1;{}r".format(self.TERMINAL_HEIGHT - 1),
|
|
"a.out | main.c:2:11 | breakpoint 1.1 ",
|
|
],
|
|
)
|
|
|
|
# Change the terminal dimensions and make sure it's reflected immediately.
|
|
self.child.setwinsize(self.TERMINAL_HEIGHT, 25)
|
|
self.child.expect(re.escape("a.out | main.c:2:11 | bre"))
|
|
self.child.setwinsize(self.TERMINAL_HEIGHT, self.TERMINAL_WIDTH)
|
|
|
|
# Change the separator.
|
|
self.expect('set set separator "S "', ["a.out S main.c:2:11"])
|
|
|
|
# Change the format.
|
|
self.expect(
|
|
'set set statusline-format "target = {${target.file.basename}} ${separator}"',
|
|
["target = a.out S"],
|
|
)
|
|
self.expect('set set separator "| "')
|
|
|
|
# Hide the statusline and check for the control character.
|
|
self.expect(
|
|
"set set show-statusline false", ["\x1b[1;{}r".format(self.TERMINAL_HEIGHT)]
|
|
)
|
|
|
|
def test_no_color(self):
|
|
"""Basic test for the statusline with colors disabled."""
|
|
self.build()
|
|
self.launch(use_colors=False)
|
|
self.do_setup()
|
|
|
|
# Enable the statusline and check for the "reverse video" control character.
|
|
self.expect(
|
|
"set set show-statusline true",
|
|
[
|
|
"\x1b[7m",
|
|
],
|
|
)
|
|
|
|
def test_deadlock(self):
|
|
"""Regression test for lock inversion between the statusline mutex and
|
|
the output mutex."""
|
|
self.build()
|
|
self.launch(extra_args=["-o", "settings set use-color false"])
|
|
self.child.expect("(lldb)")
|
|
self.resize()
|
|
|
|
exe = self.getBuildArtifact("a.out")
|
|
|
|
self.expect("file {}".format(exe), ["Current executable"])
|
|
self.expect("help", ["Debugger commands"])
|
|
|
|
def test_no_target(self):
|
|
"""Test that we print "no target" when launched without a target."""
|
|
self.launch()
|
|
self.resize()
|
|
|
|
self.expect("set set show-statusline true", ["no target"])
|
|
|
|
@skipIfEditlineSupportMissing
|
|
def test_resize(self):
|
|
"""Test that move the cursor when resizing."""
|
|
self.launch()
|
|
self.resize()
|
|
self.expect("set set show-statusline true", ["no target"])
|
|
self.resize(20, 60)
|
|
# Check for the escape code to resize the scroll window.
|
|
self.child.expect(re.escape("\x1b[1;19r"))
|
|
self.child.expect("(lldb)")
|
|
|
|
@skipUnlessPlatform(["linux"])
|
|
def test_target_symbols_add(self):
|
|
"""Regression test: adding symbols while the statusline is enabled
|
|
should not crash. The bug was a stale Symbol pointer in the cached
|
|
execution context after Symtab reallocation; best caught under ASAN."""
|
|
import shutil
|
|
import subprocess
|
|
|
|
self.build()
|
|
symfile = self.getBuildArtifact("a.out")
|
|
stripped_exe = self.getBuildArtifact("stripped.out")
|
|
shutil.copy2(symfile, stripped_exe)
|
|
subprocess.check_call(["strip", "--strip-debug", stripped_exe])
|
|
self.launch()
|
|
|
|
self.expect(
|
|
"target create {}".format(stripped_exe),
|
|
substrs=["Current executable set to"],
|
|
)
|
|
self.expect("breakpoint set -n main", substrs=["Breakpoint 1"])
|
|
self.expect("run", substrs=["stop reason"])
|
|
self.resize()
|
|
|
|
self.expect('set set separator "| "')
|
|
self.expect(
|
|
"set set show-statusline true",
|
|
["stripped.out | breakpoint 1.1"],
|
|
)
|
|
|
|
# Adding symbols triggers Process::Flush() which (with the fix)
|
|
# clears the statusline's cached execution context. The expect()
|
|
# helper waits for the (lldb) prompt, which triggers a statusline
|
|
# redraw via Redraw(std::nullopt) using the cached (now cleared)
|
|
# context. Under ASAN, a stale Symbol* here would crash.
|
|
self.expect(
|
|
"target symbols add -s {} {}".format(stripped_exe, symfile),
|
|
["has been added to"],
|
|
)
|
|
|
|
@skipIfRemote
|
|
@skipIfWindows
|
|
@skipIfDarwin
|
|
@skipIfLinux # https://github.com/llvm/llvm-project/issues/154763
|
|
@add_test_categories(["lldb-server"])
|
|
def test_modulelist_deadlock(self):
|
|
"""Regression test for a deadlock that occurs when the status line is enabled before connecting
|
|
to a gdb-remote server.
|
|
"""
|
|
if get_lldb_server_exe() is None:
|
|
self.skipTest("lldb-server not found")
|
|
|
|
MAX_RETRY_ATTEMPTS = 10
|
|
DELAY = 0.25
|
|
|
|
def _find_free_port(host):
|
|
for attempt in range(MAX_RETRY_ATTEMPTS):
|
|
try:
|
|
family, type, proto, _, _ = socket.getaddrinfo(
|
|
host, 0, proto=socket.IPPROTO_TCP
|
|
)[0]
|
|
with closing(socket.socket(family, type, proto)) as sock:
|
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
sock.bind((host, 0))
|
|
return sock.getsockname()
|
|
except OSError:
|
|
time.sleep(DELAY * 2**attempt) # Exponential backoff
|
|
raise RuntimeError(
|
|
"Could not find a free port on {} after {} attempts.".format(
|
|
host, MAX_RETRY_ATTEMPTS
|
|
)
|
|
)
|
|
|
|
def _wait_for_server_ready_in_log(log_file_path, ready_message):
|
|
"""Check log file for server ready message with retry logic"""
|
|
for attempt in range(MAX_RETRY_ATTEMPTS):
|
|
if os.path.exists(log_file_path):
|
|
with open(log_file_path, "r") as f:
|
|
if ready_message in f.read():
|
|
return
|
|
time.sleep(pow(2, attempt) * DELAY)
|
|
raise RuntimeError(
|
|
"Server not ready after {} attempts.".format(MAX_RETRY_ATTEMPTS)
|
|
)
|
|
|
|
self.build()
|
|
exe_path = self.getBuildArtifact("a.out")
|
|
server_log_file = self.getLogBasenameForCurrentTest() + "-lldbserver.log"
|
|
self.addTearDownHook(
|
|
lambda: os.path.exists(server_log_file) and os.remove(server_log_file)
|
|
)
|
|
|
|
# Find a free port for the server
|
|
addr = _find_free_port("localhost")
|
|
connect_address = "[{}]:{}".format(*addr)
|
|
commandline_args = [
|
|
"gdbserver",
|
|
connect_address,
|
|
exe_path,
|
|
"--log-file={}".format(server_log_file),
|
|
"--log-channels=lldb conn",
|
|
]
|
|
|
|
server_proc = self.spawnSubprocess(
|
|
get_lldb_server_exe(), commandline_args, install_remote=False
|
|
)
|
|
self.assertIsNotNone(server_proc)
|
|
|
|
# Wait for server to be ready by checking log file.
|
|
server_ready_message = "Listen to {}".format(connect_address)
|
|
_wait_for_server_ready_in_log(server_log_file, server_ready_message)
|
|
|
|
# Launch LLDB client and connect to lldb-server with statusline enabled
|
|
self.launch()
|
|
self.resize()
|
|
self.expect("settings set show-statusline true", ["no target"])
|
|
self.expect(
|
|
f"gdb-remote {connect_address}", [b"a.out \xe2\x94\x82 signal SIGSTOP"]
|
|
)
|