1
2from lldbsuite.test.lldbtest import *
3import os
4import vscode
5
6
7class VSCodeTestCaseBase(TestBase):
8
9    NO_DEBUG_INFO_TESTCASE = True
10
11    def create_debug_adaptor(self):
12        '''Create the Visual Studio Code debug adaptor'''
13        self.assertTrue(os.path.exists(self.lldbVSCodeExec),
14                        'lldb-vscode must exist')
15        log_file_path = self.getBuildArtifact('vscode.txt')
16        self.vscode = vscode.DebugAdaptor(
17            executable=self.lldbVSCodeExec, init_commands=self.setUpCommands(),
18            log_file=log_file_path)
19
20    def build_and_create_debug_adaptor(self):
21        self.build()
22        self.create_debug_adaptor()
23
24    def set_source_breakpoints(self, source_path, lines, condition=None,
25                               hitCondition=None):
26        '''Sets source breakpoints and returns an array of strings containing
27           the breakpoint IDs ("1", "2") for each breakpoint that was set.
28        '''
29        response = self.vscode.request_setBreakpoints(
30            source_path, lines, condition=condition, hitCondition=hitCondition)
31        if response is None:
32            return []
33        breakpoints = response['body']['breakpoints']
34        breakpoint_ids = []
35        for breakpoint in breakpoints:
36            breakpoint_ids.append('%i' % (breakpoint['id']))
37        return breakpoint_ids
38
39    def set_function_breakpoints(self, functions, condition=None,
40                                 hitCondition=None):
41        '''Sets breakpoints by function name given an array of function names
42           and returns an array of strings containing the breakpoint IDs
43           ("1", "2") for each breakpoint that was set.
44        '''
45        response = self.vscode.request_setFunctionBreakpoints(
46            functions, condition=condition, hitCondition=hitCondition)
47        if response is None:
48            return []
49        breakpoints = response['body']['breakpoints']
50        breakpoint_ids = []
51        for breakpoint in breakpoints:
52            breakpoint_ids.append('%i' % (breakpoint['id']))
53        return breakpoint_ids
54
55    def verify_breakpoint_hit(self, breakpoint_ids):
56        '''Wait for the process we are debugging to stop, and verify we hit
57           any breakpoint location in the "breakpoint_ids" array.
58           "breakpoint_ids" should be a list of breakpoint ID strings
59           (["1", "2"]). The return value from self.set_source_breakpoints()
60           or self.set_function_breakpoints() can be passed to this function'''
61        stopped_events = self.vscode.wait_for_stopped()
62        for stopped_event in stopped_events:
63            if 'body' in stopped_event:
64                body = stopped_event['body']
65                if 'reason' not in body:
66                    continue
67                if body['reason'] != 'breakpoint':
68                    continue
69                if 'description' not in body:
70                    continue
71                # Descriptions for breakpoints will be in the form
72                # "breakpoint 1.1", so look for any description that matches
73                # ("breakpoint 1.") in the description field as verification
74                # that one of the breakpoint locations was hit. VSCode doesn't
75                # allow breakpoints to have multiple locations, but LLDB does.
76                # So when looking at the description we just want to make sure
77                # the right breakpoint matches and not worry about the actual
78                # location.
79                description = body['description']
80                print("description: %s" % (description))
81                for breakpoint_id in breakpoint_ids:
82                    match_desc = 'breakpoint %s.' % (breakpoint_id)
83                    if match_desc in description:
84                        return
85        self.assertTrue(False, "breakpoint not hit")
86
87    def verify_exception_breakpoint_hit(self, filter_label):
88        '''Wait for the process we are debugging to stop, and verify the stop
89           reason is 'exception' and that the description matches
90           'filter_label'
91        '''
92        stopped_events = self.vscode.wait_for_stopped()
93        for stopped_event in stopped_events:
94            if 'body' in stopped_event:
95                body = stopped_event['body']
96                if 'reason' not in body:
97                    continue
98                if body['reason'] != 'exception':
99                    continue
100                if 'description' not in body:
101                    continue
102                description = body['description']
103                if filter_label == description:
104                    return True
105        return False
106
107    def verify_commands(self, flavor, output, commands):
108        self.assertTrue(output and len(output) > 0, "expect console output")
109        lines = output.splitlines()
110        prefix = '(lldb) '
111        for cmd in commands:
112            found = False
113            for line in lines:
114                if line.startswith(prefix) and cmd in line:
115                    found = True
116                    break
117            self.assertTrue(found,
118                            "verify '%s' found in console output for '%s'" % (
119                                cmd, flavor))
120
121    def get_dict_value(self, d, key_path):
122        '''Verify each key in the key_path array is in contained in each
123           dictionary within "d". Assert if any key isn't in the
124           corresponding dictionary. This is handy for grabbing values from VS
125           Code response dictionary like getting
126           response['body']['stackFrames']
127        '''
128        value = d
129        for key in key_path:
130            if key in value:
131                value = value[key]
132            else:
133                self.assertTrue(key in value,
134                                'key "%s" from key_path "%s" not in "%s"' % (
135                                    key, key_path, d))
136        return value
137
138    def get_stackFrames_and_totalFramesCount(self, threadId=None, startFrame=None,
139                        levels=None, dump=False):
140        response = self.vscode.request_stackTrace(threadId=threadId,
141                                                  startFrame=startFrame,
142                                                  levels=levels,
143                                                  dump=dump)
144        if response:
145            stackFrames = self.get_dict_value(response, ['body', 'stackFrames'])
146            totalFrames = self.get_dict_value(response, ['body', 'totalFrames'])
147            self.assertTrue(totalFrames > 0,
148                    'verify totalFrames count is provided by extension that supports '
149                    'async frames loading')
150            return (stackFrames, totalFrames)
151        return (None, 0)
152
153    def get_stackFrames(self, threadId=None, startFrame=None, levels=None,
154                        dump=False):
155        (stackFrames, totalFrames) = self.get_stackFrames_and_totalFramesCount(
156                                                threadId=threadId,
157                                                startFrame=startFrame,
158                                                levels=levels,
159                                                dump=dump)
160        return stackFrames
161
162    def get_source_and_line(self, threadId=None, frameIndex=0):
163        stackFrames = self.get_stackFrames(threadId=threadId,
164                                           startFrame=frameIndex,
165                                           levels=1)
166        if stackFrames is not None:
167            stackFrame = stackFrames[0]
168            ['source', 'path']
169            if 'source' in stackFrame:
170                source = stackFrame['source']
171                if 'path' in source:
172                    if 'line' in stackFrame:
173                        return (source['path'], stackFrame['line'])
174        return ('', 0)
175
176    def get_stdout(self, timeout=0.0):
177        return self.vscode.get_output('stdout', timeout=timeout)
178
179    def get_console(self, timeout=0.0):
180        return self.vscode.get_output('console', timeout=timeout)
181
182    def get_local_as_int(self, name, threadId=None):
183        value = self.vscode.get_local_variable_value(name, threadId=threadId)
184        if value.startswith('0x'):
185            return int(value, 16)
186        elif value.startswith('0'):
187            return int(value, 8)
188        else:
189            return int(value)
190
191    def set_local(self, name, value, id=None):
192        '''Set a top level local variable only.'''
193        return self.vscode.request_setVariable(1, name, str(value), id=id)
194
195    def set_global(self, name, value, id=None):
196        '''Set a top level global variable only.'''
197        return self.vscode.request_setVariable(2, name, str(value), id=id)
198
199    def stepIn(self, threadId=None, waitForStop=True):
200        self.vscode.request_stepIn(threadId=threadId)
201        if waitForStop:
202            return self.vscode.wait_for_stopped()
203        return None
204
205    def stepOver(self, threadId=None, waitForStop=True):
206        self.vscode.request_next(threadId=threadId)
207        if waitForStop:
208            return self.vscode.wait_for_stopped()
209        return None
210
211    def stepOut(self, threadId=None, waitForStop=True):
212        self.vscode.request_stepOut(threadId=threadId)
213        if waitForStop:
214            return self.vscode.wait_for_stopped()
215        return None
216
217    def continue_to_next_stop(self):
218        self.vscode.request_continue()
219        return self.vscode.wait_for_stopped()
220
221    def continue_to_breakpoints(self, breakpoint_ids):
222        self.vscode.request_continue()
223        self.verify_breakpoint_hit(breakpoint_ids)
224
225    def continue_to_exception_breakpoint(self, filter_label):
226        self.vscode.request_continue()
227        self.assertTrue(self.verify_exception_breakpoint_hit(filter_label),
228                        'verify we got "%s"' % (filter_label))
229
230    def continue_to_exit(self, exitCode=0):
231        self.vscode.request_continue()
232        stopped_events = self.vscode.wait_for_stopped()
233        self.assertEquals(len(stopped_events), 1,
234                        "stopped_events = {}".format(stopped_events))
235        self.assertEquals(stopped_events[0]['event'], 'exited',
236                        'make sure program ran to completion')
237        self.assertEquals(stopped_events[0]['body']['exitCode'], exitCode,
238                        'exitCode == %i' % (exitCode))
239
240    def attach(self, program=None, pid=None, waitFor=None, trace=None,
241               initCommands=None, preRunCommands=None, stopCommands=None,
242               exitCommands=None, attachCommands=None):
243        '''Build the default Makefile target, create the VSCode debug adaptor,
244           and attach to the process.
245        '''
246        # Make sure we disconnect and terminate the VSCode debug adaptor even
247        # if we throw an exception during the test case.
248        def cleanup():
249            self.vscode.request_disconnect(terminateDebuggee=True)
250            self.vscode.terminate()
251
252        # Execute the cleanup function during test case tear down.
253        self.addTearDownHook(cleanup)
254        # Initialize and launch the program
255        self.vscode.request_initialize()
256        response = self.vscode.request_attach(
257            program=program, pid=pid, waitFor=waitFor, trace=trace,
258            initCommands=initCommands, preRunCommands=preRunCommands,
259            stopCommands=stopCommands, exitCommands=exitCommands,
260            attachCommands=attachCommands)
261        if not (response and response['success']):
262            self.assertTrue(response['success'],
263                            'attach failed (%s)' % (response['message']))
264
265    def launch(self, program=None, args=None, cwd=None, env=None,
266               stopOnEntry=False, disableASLR=True,
267               disableSTDIO=False, shellExpandArguments=False,
268               trace=False, initCommands=None, preRunCommands=None,
269               stopCommands=None, exitCommands=None,sourcePath=None,
270               debuggerRoot=None, launchCommands=None, sourceMap=None):
271        '''Sending launch request to vscode
272        '''
273
274        # Make sure we disconnect and terminate the VSCode debug adapter,
275        # if we throw an exception during the test case
276        def cleanup():
277            self.vscode.request_disconnect(terminateDebuggee=True)
278            self.vscode.terminate()
279
280        # Execute the cleanup function during test case tear down.
281        self.addTearDownHook(cleanup)
282
283        # Initialize and launch the program
284        self.vscode.request_initialize()
285        response = self.vscode.request_launch(
286            program,
287            args=args,
288            cwd=cwd,
289            env=env,
290            stopOnEntry=stopOnEntry,
291            disableASLR=disableASLR,
292            disableSTDIO=disableSTDIO,
293            shellExpandArguments=shellExpandArguments,
294            trace=trace,
295            initCommands=initCommands,
296            preRunCommands=preRunCommands,
297            stopCommands=stopCommands,
298            exitCommands=exitCommands,
299            sourcePath=sourcePath,
300            debuggerRoot=debuggerRoot,
301            launchCommands=launchCommands,
302            sourceMap=sourceMap)
303        if not (response and response['success']):
304            self.assertTrue(response['success'],
305                            'launch failed (%s)' % (response['message']))
306
307    def build_and_launch(self, program, args=None, cwd=None, env=None,
308                         stopOnEntry=False, disableASLR=True,
309                         disableSTDIO=False, shellExpandArguments=False,
310                         trace=False, initCommands=None, preRunCommands=None,
311                         stopCommands=None, exitCommands=None,
312                         sourcePath=None, debuggerRoot=None):
313        '''Build the default Makefile target, create the VSCode debug adaptor,
314           and launch the process.
315        '''
316        self.build_and_create_debug_adaptor()
317        self.assertTrue(os.path.exists(program), 'executable must exist')
318
319        self.launch(program, args, cwd, env, stopOnEntry, disableASLR,
320                    disableSTDIO, shellExpandArguments, trace,
321                    initCommands, preRunCommands, stopCommands, exitCommands,
322                    sourcePath, debuggerRoot)
323