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 collect_console(self, duration):
183        return self.vscode.collect_output('console', duration=duration)
184
185    def get_local_as_int(self, name, threadId=None):
186        value = self.vscode.get_local_variable_value(name, threadId=threadId)
187        if value.startswith('0x'):
188            return int(value, 16)
189        elif value.startswith('0'):
190            return int(value, 8)
191        else:
192            return int(value)
193
194    def set_local(self, name, value, id=None):
195        '''Set a top level local variable only.'''
196        return self.vscode.request_setVariable(1, name, str(value), id=id)
197
198    def set_global(self, name, value, id=None):
199        '''Set a top level global variable only.'''
200        return self.vscode.request_setVariable(2, name, str(value), id=id)
201
202    def stepIn(self, threadId=None, waitForStop=True):
203        self.vscode.request_stepIn(threadId=threadId)
204        if waitForStop:
205            return self.vscode.wait_for_stopped()
206        return None
207
208    def stepOver(self, threadId=None, waitForStop=True):
209        self.vscode.request_next(threadId=threadId)
210        if waitForStop:
211            return self.vscode.wait_for_stopped()
212        return None
213
214    def stepOut(self, threadId=None, waitForStop=True):
215        self.vscode.request_stepOut(threadId=threadId)
216        if waitForStop:
217            return self.vscode.wait_for_stopped()
218        return None
219
220    def continue_to_next_stop(self):
221        self.vscode.request_continue()
222        return self.vscode.wait_for_stopped()
223
224    def continue_to_breakpoints(self, breakpoint_ids):
225        self.vscode.request_continue()
226        self.verify_breakpoint_hit(breakpoint_ids)
227
228    def continue_to_exception_breakpoint(self, filter_label):
229        self.vscode.request_continue()
230        self.assertTrue(self.verify_exception_breakpoint_hit(filter_label),
231                        'verify we got "%s"' % (filter_label))
232
233    def continue_to_exit(self, exitCode=0):
234        self.vscode.request_continue()
235        stopped_events = self.vscode.wait_for_stopped()
236        self.assertEquals(len(stopped_events), 1,
237                        "stopped_events = {}".format(stopped_events))
238        self.assertEquals(stopped_events[0]['event'], 'exited',
239                        'make sure program ran to completion')
240        self.assertEquals(stopped_events[0]['body']['exitCode'], exitCode,
241                        'exitCode == %i' % (exitCode))
242
243    def attach(self, program=None, pid=None, waitFor=None, trace=None,
244               initCommands=None, preRunCommands=None, stopCommands=None,
245               exitCommands=None, attachCommands=None, coreFile=None,
246               disconnectAutomatically=True, terminateCommands=None):
247        '''Build the default Makefile target, create the VSCode debug adaptor,
248           and attach to the process.
249        '''
250        # Make sure we disconnect and terminate the VSCode debug adaptor even
251        # if we throw an exception during the test case.
252        def cleanup():
253            if disconnectAutomatically:
254                self.vscode.request_disconnect(terminateDebuggee=True)
255            self.vscode.terminate()
256
257        # Execute the cleanup function during test case tear down.
258        self.addTearDownHook(cleanup)
259        # Initialize and launch the program
260        self.vscode.request_initialize()
261        response = self.vscode.request_attach(
262            program=program, pid=pid, waitFor=waitFor, trace=trace,
263            initCommands=initCommands, preRunCommands=preRunCommands,
264            stopCommands=stopCommands, exitCommands=exitCommands,
265            attachCommands=attachCommands, terminateCommands=terminateCommands,
266            coreFile=coreFile)
267        if not (response and response['success']):
268            self.assertTrue(response['success'],
269                            'attach failed (%s)' % (response['message']))
270
271    def launch(self, program=None, args=None, cwd=None, env=None,
272               stopOnEntry=False, disableASLR=True,
273               disableSTDIO=False, shellExpandArguments=False,
274               trace=False, initCommands=None, preRunCommands=None,
275               stopCommands=None, exitCommands=None, terminateCommands=None,
276               sourcePath=None, debuggerRoot=None, launchCommands=None,
277               sourceMap=None, disconnectAutomatically=True):
278        '''Sending launch request to vscode
279        '''
280
281        # Make sure we disconnect and terminate the VSCode debug adapter,
282        # if we throw an exception during the test case
283        def cleanup():
284            if disconnectAutomatically:
285                self.vscode.request_disconnect(terminateDebuggee=True)
286            self.vscode.terminate()
287
288        # Execute the cleanup function during test case tear down.
289        self.addTearDownHook(cleanup)
290
291        # Initialize and launch the program
292        self.vscode.request_initialize()
293        response = self.vscode.request_launch(
294            program,
295            args=args,
296            cwd=cwd,
297            env=env,
298            stopOnEntry=stopOnEntry,
299            disableASLR=disableASLR,
300            disableSTDIO=disableSTDIO,
301            shellExpandArguments=shellExpandArguments,
302            trace=trace,
303            initCommands=initCommands,
304            preRunCommands=preRunCommands,
305            stopCommands=stopCommands,
306            exitCommands=exitCommands,
307            terminateCommands=terminateCommands,
308            sourcePath=sourcePath,
309            debuggerRoot=debuggerRoot,
310            launchCommands=launchCommands,
311            sourceMap=sourceMap)
312        if not (response and response['success']):
313            self.assertTrue(response['success'],
314                            'launch failed (%s)' % (response['message']))
315
316    def build_and_launch(self, program, args=None, cwd=None, env=None,
317                         stopOnEntry=False, disableASLR=True,
318                         disableSTDIO=False, shellExpandArguments=False,
319                         trace=False, initCommands=None, preRunCommands=None,
320                         stopCommands=None, exitCommands=None,
321                         terminateCommands=None, sourcePath=None,
322                         debuggerRoot=None):
323        '''Build the default Makefile target, create the VSCode debug adaptor,
324           and launch the process.
325        '''
326        self.build_and_create_debug_adaptor()
327        self.assertTrue(os.path.exists(program), 'executable must exist')
328
329        self.launch(program, args, cwd, env, stopOnEntry, disableASLR,
330                    disableSTDIO, shellExpandArguments, trace,
331                    initCommands, preRunCommands, stopCommands, exitCommands,
332                    terminateCommands, sourcePath, debuggerRoot)
333