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