aboutsummaryrefslogtreecommitdiff
path: root/lldb/test/API/functionalities/statusline/TestStatusline.py
blob: 4ffa864120a5c88dbf0cab59ac2540c1baf7afef (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
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):
    # Change this value to something smaller to make debugging this test less
    # tedious.
    TIMEOUT = 60

    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(timeout=self.TIMEOUT)
        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, timeout=self.TIMEOUT)
        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"], timeout=self.TIMEOUT
        )
        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(timeout=self.TIMEOUT)
        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(timeout=self.TIMEOUT)
        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)")

    @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(timeout=self.TIMEOUT)
        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"]
        )