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
12from __future__ import print_function
13
14import lldb
15from lldbsuite.test.decorators import *
16from lldbsuite.test.lldbtest import *
17from lldbsuite.test import lldbutil
18
19
20def ansi_underline_surround_regex(inner_regex_text):
21    # return re.compile(r"\[4m%s\[0m" % inner_regex_text)
22    return "4.+\033\\[4m%s\033\\[0m" % inner_regex_text
23
24def ansi_color_surround_regex(inner_regex_text):
25    return "\033\\[3[0-7]m%s\033\\[0m" % inner_regex_text
26
27class SourceManagerTestCase(TestBase):
28
29    mydir = TestBase.compute_mydir(__file__)
30
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(self, use_color, needle_regex, highlight_source=False):
52        self.build()
53        exe = self.getBuildArtifact("a.out")
54        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)
55
56        target = self.dbg.CreateTarget(exe)
57        self.assertTrue(target, VALID_TARGET)
58
59        # Launch the process, and do not stop at the entry point.
60        args = None
61        envp = None
62        process = target.LaunchSimple(
63            args, envp, self.get_process_working_directory())
64        self.assertIsNotNone(process)
65
66        #
67        # Exercise Python APIs to display source lines.
68        #
69
70        # Setup whether we should use ansi escape sequences, including color
71        # and styles such as underline.
72        self.dbg.SetUseColor(use_color)
73        # Disable syntax highlighting if needed.
74
75        self.runCmd("settings set highlight-source " + str(highlight_source).lower())
76
77        filespec = lldb.SBFileSpec(self.file, False)
78        source_mgr = self.dbg.GetSourceManager()
79        # Use a string stream as the destination.
80        stream = lldb.SBStream()
81        column = self.get_expected_stop_column_number()
82        context_before = 2
83        context_after = 2
84        current_line_prefix = "=>"
85        source_mgr.DisplaySourceLinesWithLineNumbersAndColumn(
86            filespec, self.line, column, context_before, context_after,
87            current_line_prefix, stream)
88
89        #    2
90        #    3    int main(int argc, char const *argv[]) {
91        # => 4        printf("Hello world.\n"); // Set break point at this line.
92        #    5        return 0;
93        #    6    }
94        self.expect(stream.GetData(), "Source code displayed correctly:\n" + stream.GetData(),
95                    exe=False,
96                    patterns=['=>', '%d.*Hello world' % self.line,
97                              needle_regex])
98
99        # Boundary condition testings for SBStream().  LLDB should not crash!
100        stream.Print(None)
101        stream.RedirectToFile(None, True)
102
103    @add_test_categories(['pyapi'])
104    def test_display_source_python_dumb_terminal(self):
105        """Test display of source using the SBSourceManager API, using a
106        dumb terminal and thus no color support (the default)."""
107        use_color = False
108        self.do_display_source_python_api(use_color, r"\s+\^")
109
110    @add_test_categories(['pyapi'])
111    def test_display_source_python_ansi_terminal(self):
112        """Test display of source using the SBSourceManager API, using a
113        dumb terminal and thus no color support (the default)."""
114        use_color = True
115        underline_regex = ansi_underline_surround_regex(r"printf")
116        self.do_display_source_python_api(use_color, underline_regex)
117
118    @add_test_categories(['pyapi'])
119    def test_display_source_python_ansi_terminal_syntax_highlighting(self):
120        """Test display of source using the SBSourceManager API and check for
121        the syntax highlighted output"""
122        use_color = True
123        syntax_highlighting = True;
124
125        # Just pick 'int' as something that should be colored.
126        color_regex = ansi_color_surround_regex("int")
127        self.do_display_source_python_api(use_color, color_regex, syntax_highlighting)
128
129        # Same for 'char'.
130        color_regex = ansi_color_surround_regex("char")
131        self.do_display_source_python_api(use_color, color_regex, syntax_highlighting)
132
133        # Test that we didn't color unrelated identifiers.
134        self.do_display_source_python_api(use_color, r" main\(", syntax_highlighting)
135        self.do_display_source_python_api(use_color, r"\);", syntax_highlighting)
136
137    def test_move_and_then_display_source(self):
138        """Test that target.source-map settings work by moving main.c to hidden/main.c."""
139        self.build()
140        exe = self.getBuildArtifact("a.out")
141        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)
142
143        # Move main.c to hidden/main.c.
144        hidden = self.getBuildArtifact("hidden")
145        lldbutil.mkdir_p(hidden)
146        main_c_hidden = os.path.join(hidden, "main-copy.c")
147        os.rename(self.file, main_c_hidden)
148
149        if self.TraceOn():
150            system([["ls"]])
151            system([["ls", "hidden"]])
152
153        # Set source remapping with invalid replace path and verify we get an
154        # error
155        self.expect(
156            "settings set target.source-map /a/b/c/d/e /q/r/s/t/u",
157            error=True,
158            substrs=['''error: the replacement path doesn't exist: "/q/r/s/t/u"'''])
159
160        # 'make -C' has resolved current directory to its realpath form.
161        builddir_real = os.path.realpath(self.getBuildDir())
162        hidden_real = os.path.realpath(hidden)
163        # Set target.source-map settings.
164        self.runCmd("settings set target.source-map %s %s" %
165                    (builddir_real, hidden_real))
166        # And verify that the settings work.
167        self.expect("settings show target.source-map",
168                    substrs=[builddir_real, hidden_real])
169
170        # Display main() and verify that the source mapping has been kicked in.
171        self.expect("source list -n main", SOURCE_DISPLAYED_CORRECTLY,
172                    substrs=['Hello world'])
173
174    @skipIf(oslist=["windows"], bugnumber="llvm.org/pr44431")
175    @skipIfReproducer # VFS is a snapshot.
176    def test_modify_source_file_while_debugging(self):
177        """Modify a source file while debugging the executable."""
178        self.build()
179        exe = self.getBuildArtifact("a.out")
180        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)
181
182        lldbutil.run_break_set_by_file_and_line(
183            self, "main-copy.c", self.line, num_expected_locations=1, loc_exact=True)
184
185        self.runCmd("run", RUN_SUCCEEDED)
186
187        # The stop reason of the thread should be breakpoint.
188        self.expect("thread list", STOPPED_DUE_TO_BREAKPOINT,
189                    substrs=['stopped',
190                             'main-copy.c:%d' % self.line,
191                             'stop reason = breakpoint'])
192
193        # Display some source code.
194        self.expect(
195            "source list -f main-copy.c -l %d" %
196            self.line,
197            SOURCE_DISPLAYED_CORRECTLY,
198            substrs=['Hello world'])
199
200        # Do the same thing with a file & line spec:
201        self.expect(
202            "source list -y main-copy.c:%d" %
203            self.line,
204            SOURCE_DISPLAYED_CORRECTLY,
205            substrs=['Hello world'])
206
207
208        # The '-b' option shows the line table locations from the debug information
209        # that indicates valid places to set source level breakpoints.
210
211        # The file to display is implicit in this case.
212        self.runCmd("source list -l %d -c 3 -b" % self.line)
213        output = self.res.GetOutput().splitlines()[0]
214
215        # If the breakpoint set command succeeded, we should expect a positive number
216        # of breakpoints for the current line, i.e., self.line.
217        import re
218        m = re.search('^\[(\d+)\].*// Set break point at this line.', output)
219        if not m:
220            self.fail("Fail to display source level breakpoints")
221        self.assertTrue(int(m.group(1)) > 0)
222
223        # Read the main.c file content.
224        with io.open(self.file, 'r', newline='\n') as f:
225            original_content = f.read()
226            if self.TraceOn():
227                print("original content:", original_content)
228
229        # Modify the in-memory copy of the original source code.
230        new_content = original_content.replace('Hello world', 'Hello lldb', 1)
231
232        # Modify the source code file.
233        with io.open(self.file, 'w', newline='\n') as f:
234            time.sleep(1)
235            f.write(new_content)
236            if self.TraceOn():
237                print("new content:", new_content)
238                print(
239                    "os.path.getmtime() after writing new content:",
240                    os.path.getmtime(self.file))
241
242        # Display the source code again.  We should see the updated line.
243        self.expect(
244            "source list -f main-copy.c -l %d" %
245            self.line,
246            SOURCE_DISPLAYED_CORRECTLY,
247            substrs=['Hello lldb'])
248
249    def test_set_breakpoint_with_absolute_path(self):
250        self.build()
251        hidden = self.getBuildArtifact("hidden")
252        lldbutil.mkdir_p(hidden)
253        # 'make -C' has resolved current directory to its realpath form.
254        builddir_real = os.path.realpath(self.getBuildDir())
255        hidden_real = os.path.realpath(hidden)
256        self.runCmd("settings set target.source-map %s %s" %
257                    (builddir_real, hidden_real))
258
259        exe = self.getBuildArtifact("a.out")
260        main = os.path.join(builddir_real, "hidden", "main-copy.c")
261        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)
262
263        lldbutil.run_break_set_by_file_and_line(
264            self, main, self.line, num_expected_locations=1, loc_exact=False)
265
266        self.runCmd("run", RUN_SUCCEEDED)
267
268        # The stop reason of the thread should be breakpoint.
269        self.expect("thread list", STOPPED_DUE_TO_BREAKPOINT,
270                    substrs=['stopped',
271                             'main-copy.c:%d' % self.line,
272                             'stop reason = breakpoint'])
273