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