xref: /llvm-project/lldb/test/API/source-manager/TestSourceManager.py (revision d49caf4afc1bc460600b316f9736e01ceeebded5)
1"""
2Test lldb core component: SourceManager.
3
4Test cases:
5
6o test_display_source_python:
7  Test display of source using the SBSourceManager API.
8o test_modify_source_file_while_debugging:
9  Test the caching mechanism of the source manager.
10"""
11
12import os
13import stat
14
15import lldb
16from lldbsuite.test.decorators import *
17from lldbsuite.test.lldbtest import *
18from lldbsuite.test import lldbutil
19
20
21def ansi_underline_surround_regex(inner_regex_text):
22    # return re.compile(r"\[4m%s\[0m" % inner_regex_text)
23    return "4.+\033\\[4m%s\033\\[0m" % inner_regex_text
24
25
26def ansi_color_surround_regex(inner_regex_text):
27    return "\033\\[3[0-7]m%s\033\\[0m" % inner_regex_text
28
29
30class SourceManagerTestCase(TestBase):
31    NO_DEBUG_INFO_TESTCASE = True
32
33    def setUp(self):
34        # Call super's setUp().
35        TestBase.setUp(self)
36        # Find the line number to break inside main().
37        self.file = self.getBuildArtifact("main-copy.c")
38        self.line = line_number("main.c", "// Set break point at this line.")
39
40    def get_expected_stop_column_number(self):
41        """Return the 1-based column number of the first non-whitespace
42        character in the breakpoint source line."""
43        stop_line = get_line(self.file, self.line)
44        # The number of spaces that must be skipped to get to the first non-
45        # whitespace character --- where we expect the debugger breakpoint
46        # column to be --- is equal to the number of characters that get
47        # stripped off the front when we lstrip it, plus one to specify
48        # the character column after the initial whitespace.
49        return len(stop_line) - len(stop_line.lstrip()) + 1
50
51    def do_display_source_python_api(
52        self, use_color, needle_regex, highlight_source=False
53    ):
54        self.build()
55        exe = self.getBuildArtifact("a.out")
56        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)
57
58        target = self.dbg.CreateTarget(exe)
59        self.assertTrue(target, VALID_TARGET)
60
61        # Launch the process, and do not stop at the entry point.
62        args = None
63        envp = None
64        process = target.LaunchSimple(args, envp, self.get_process_working_directory())
65        self.assertIsNotNone(process)
66
67        #
68        # Exercise Python APIs to display source lines.
69        #
70
71        # Setup whether we should use ansi escape sequences, including color
72        # and styles such as underline.
73        self.dbg.SetUseColor(use_color)
74        # Disable syntax highlighting if needed.
75
76        self.runCmd("settings set highlight-source " + str(highlight_source).lower())
77
78        filespec = lldb.SBFileSpec(self.file, False)
79        source_mgr = self.dbg.GetSourceManager()
80        # Use a string stream as the destination.
81        stream = lldb.SBStream()
82        column = self.get_expected_stop_column_number()
83        context_before = 2
84        context_after = 2
85        current_line_prefix = "=>"
86        source_mgr.DisplaySourceLinesWithLineNumbersAndColumn(
87            filespec,
88            self.line,
89            column,
90            context_before,
91            context_after,
92            current_line_prefix,
93            stream,
94        )
95
96        #    2
97        #    3    int main(int argc, char const *argv[]) {
98        # => 4        printf("Hello world.\n"); // Set break point at this line.
99        #    5        return 0;
100        #    6    }
101        self.expect(
102            stream.GetData(),
103            "Source code displayed correctly:\n" + stream.GetData(),
104            exe=False,
105            patterns=["=>", "%d.*Hello world" % self.line, needle_regex],
106        )
107
108        # Boundary condition testings for SBStream().  LLDB should not crash!
109        stream.Print(None)
110        stream.RedirectToFile(None, True)
111
112    @add_test_categories(["pyapi"])
113    def test_display_source_python_dumb_terminal(self):
114        """Test display of source using the SBSourceManager API, using a
115        dumb terminal and thus no color support (the default)."""
116        use_color = False
117        self.do_display_source_python_api(use_color, r"\s+\^")
118
119    @add_test_categories(["pyapi"])
120    def test_display_source_python_ansi_terminal(self):
121        """Test display of source using the SBSourceManager API, using a
122        dumb terminal and thus no color support (the default)."""
123        use_color = True
124        underline_regex = ansi_underline_surround_regex(r"printf")
125        self.do_display_source_python_api(use_color, underline_regex)
126
127    @add_test_categories(["pyapi"])
128    def test_display_source_python_ansi_terminal_syntax_highlighting(self):
129        """Test display of source using the SBSourceManager API and check for
130        the syntax highlighted output"""
131        use_color = True
132        syntax_highlighting = True
133
134        # Just pick 'int' as something that should be colored.
135        color_regex = ansi_color_surround_regex("int")
136        self.do_display_source_python_api(use_color, color_regex, syntax_highlighting)
137
138        # Same for 'char'.
139        color_regex = ansi_color_surround_regex("char")
140        self.do_display_source_python_api(use_color, color_regex, syntax_highlighting)
141
142        # Test that we didn't color unrelated identifiers.
143        self.do_display_source_python_api(use_color, r" main\(", syntax_highlighting)
144        self.do_display_source_python_api(use_color, r"\);", syntax_highlighting)
145
146    def test_move_and_then_display_source(self):
147        """Test that target.source-map settings work by moving main.c to hidden/main.c."""
148        self.build()
149        exe = self.getBuildArtifact("a.out")
150        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)
151
152        # Move main.c to hidden/main.c.
153        hidden = self.getBuildArtifact("hidden")
154        lldbutil.mkdir_p(hidden)
155        main_c_hidden = os.path.join(hidden, "main-copy.c")
156        os.rename(self.file, main_c_hidden)
157
158        # Set source remapping with invalid replace path and verify we get an
159        # error
160        self.expect(
161            "settings set target.source-map /a/b/c/d/e /q/r/s/t/u",
162            error=True,
163            substrs=['''error: the replacement path doesn't exist: "/q/r/s/t/u"'''],
164        )
165
166        # 'make -C' has resolved current directory to its realpath form.
167        builddir_real = os.path.realpath(self.getBuildDir())
168        hidden_real = os.path.realpath(hidden)
169        # Set target.source-map settings.
170        self.runCmd(
171            "settings set target.source-map %s %s" % (builddir_real, hidden_real)
172        )
173        # And verify that the settings work.
174        self.expect(
175            "settings show target.source-map", substrs=[builddir_real, hidden_real]
176        )
177
178        # Display main() and verify that the source mapping has been kicked in.
179        self.expect(
180            "source list -n main", SOURCE_DISPLAYED_CORRECTLY, substrs=["Hello world"]
181        )
182
183    @skipIf(oslist=["windows"], bugnumber="llvm.org/pr44431")
184    def test_modify_source_file_while_debugging(self):
185        """Modify a source file while debugging the executable."""
186        self.build()
187        exe = self.getBuildArtifact("a.out")
188        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)
189
190        lldbutil.run_break_set_by_file_and_line(
191            self, "main-copy.c", self.line, num_expected_locations=1, loc_exact=True
192        )
193
194        self.runCmd("run", RUN_SUCCEEDED)
195
196        # The stop reason of the thread should be breakpoint.
197        self.expect(
198            "thread list",
199            STOPPED_DUE_TO_BREAKPOINT,
200            substrs=[
201                "stopped",
202                "main-copy.c:%d" % self.line,
203                "stop reason = breakpoint",
204            ],
205        )
206
207        # Display some source code.
208        self.expect(
209            "source list -f main-copy.c -l %d" % self.line,
210            SOURCE_DISPLAYED_CORRECTLY,
211            substrs=["Hello world"],
212        )
213
214        # Do the same thing with a file & line spec:
215        self.expect(
216            "source list -y main-copy.c:%d" % self.line,
217            SOURCE_DISPLAYED_CORRECTLY,
218            substrs=["Hello world"],
219        )
220
221        # The '-b' option shows the line table locations from the debug information
222        # that indicates valid places to set source level breakpoints.
223
224        # The file to display is implicit in this case.
225        self.runCmd("source list -l %d -c 3 -b" % self.line)
226        output = self.res.GetOutput().splitlines()[0]
227
228        # If the breakpoint set command succeeded, we should expect a positive number
229        # of breakpoints for the current line, i.e., self.line.
230        import re
231
232        m = re.search("^\[(\d+)\].*// Set break point at this line.", output)
233        if not m:
234            self.fail("Fail to display source level breakpoints")
235        self.assertTrue(int(m.group(1)) > 0)
236
237        # Read the main.c file content.
238        with io.open(self.file, "r", newline="\n") as f:
239            original_content = f.read()
240            if self.TraceOn():
241                print("original content:", original_content)
242
243        # Modify the in-memory copy of the original source code.
244        new_content = original_content.replace("Hello world", "Hello lldb", 1)
245
246        # Modify the source code file.
247        # If the source was read only, the copy will also be read only.
248        # Run "chmod u+w" on it first so we can modify it.
249        statinfo = os.stat(self.file)
250        os.chmod(self.file, statinfo.st_mode | stat.S_IWUSR)
251
252        with io.open(self.file, "w", newline="\n") as f:
253            time.sleep(1)
254            f.write(new_content)
255            if self.TraceOn():
256                print("new content:", new_content)
257                print(
258                    "os.path.getmtime() after writing new content:",
259                    os.path.getmtime(self.file),
260                )
261
262        # Display the source code again.  We should see the updated line.
263        self.expect(
264            "source list -f main-copy.c -l %d" % self.line,
265            SOURCE_DISPLAYED_CORRECTLY,
266            substrs=["Hello lldb"],
267        )
268
269    def test_set_breakpoint_with_absolute_path(self):
270        self.build()
271        hidden = self.getBuildArtifact("hidden")
272        lldbutil.mkdir_p(hidden)
273        # 'make -C' has resolved current directory to its realpath form.
274        builddir_real = os.path.realpath(self.getBuildDir())
275        hidden_real = os.path.realpath(hidden)
276        self.runCmd(
277            "settings set target.source-map %s %s" % (builddir_real, hidden_real)
278        )
279
280        exe = self.getBuildArtifact("a.out")
281        main = os.path.join(builddir_real, "hidden", "main-copy.c")
282        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)
283
284        lldbutil.run_break_set_by_file_and_line(
285            self, main, self.line, num_expected_locations=1, loc_exact=False
286        )
287
288        self.runCmd("run", RUN_SUCCEEDED)
289
290        # The stop reason of the thread should be breakpoint.
291        self.expect(
292            "thread list",
293            STOPPED_DUE_TO_BREAKPOINT,
294            substrs=[
295                "stopped",
296                "main-copy.c:%d" % self.line,
297                "stop reason = breakpoint",
298            ],
299        )
300
301    def test_artificial_source_location(self):
302        src_file = "artificial_location.c"
303        d = {"C_SOURCES": src_file}
304        self.build(dictionary=d)
305
306        lldbutil.run_to_source_breakpoint(
307            self, "main", lldb.SBFileSpec(src_file, False)
308        )
309
310        self.expect(
311            "run",
312            RUN_SUCCEEDED,
313            substrs=[
314                "stop reason = breakpoint",
315                "%s:%d" % (src_file, 0),
316                "Note: this address is compiler-generated code in " "function",
317                "that has no source code associated " "with it.",
318            ],
319        )
320
321    def test_source_cache_dump_and_clear(self):
322        self.build()
323        exe = self.getBuildArtifact("a.out")
324        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)
325        lldbutil.run_break_set_by_file_and_line(
326            self, self.file, self.line, num_expected_locations=1, loc_exact=True
327        )
328        self.runCmd("run", RUN_SUCCEEDED)
329
330        # Make sure the main source file is in the source cache.
331        self.expect(
332            "source cache dump",
333            substrs=["Modification time", "Lines", "Path", " 7", self.file],
334        )
335
336        # Clear the cache.
337        self.expect("source cache clear")
338
339        # Make sure the main source file is no longer in the source cache.
340        self.expect("source cache dump", matching=False, substrs=[self.file])
341