1import os 2import time 3 4import dap_server 5from lldbsuite.test.lldbtest import * 6 7 8class DAPTestCaseBase(TestBase): 9 NO_DEBUG_INFO_TESTCASE = True 10 11 def create_debug_adaptor(self, lldbDAPEnv=None): 12 """Create the Visual Studio Code debug adaptor""" 13 self.assertTrue( 14 is_exe(self.lldbDAPExec), "lldb-dap must exist and be executable" 15 ) 16 log_file_path = self.getBuildArtifact("dap.txt") 17 self.dap_server = dap_server.DebugAdaptorServer( 18 executable=self.lldbDAPExec, 19 init_commands=self.setUpCommands(), 20 log_file=log_file_path, 21 env=lldbDAPEnv, 22 ) 23 24 def build_and_create_debug_adaptor(self, lldbDAPEnv=None): 25 self.build() 26 self.create_debug_adaptor(lldbDAPEnv) 27 28 def set_source_breakpoints(self, source_path, lines, data=None): 29 """Sets source breakpoints and returns an array of strings containing 30 the breakpoint IDs ("1", "2") for each breakpoint that was set. 31 Parameter data is array of data objects for breakpoints. 32 Each object in data is 1:1 mapping with the entry in lines. 33 It contains optional location/hitCondition/logMessage parameters. 34 """ 35 response = self.dap_server.request_setBreakpoints(source_path, lines, data) 36 if response is None: 37 return [] 38 breakpoints = response["body"]["breakpoints"] 39 breakpoint_ids = [] 40 for breakpoint in breakpoints: 41 breakpoint_ids.append("%i" % (breakpoint["id"])) 42 return breakpoint_ids 43 44 def set_function_breakpoints(self, functions, condition=None, hitCondition=None): 45 """Sets breakpoints by function name given an array of function names 46 and returns an array of strings containing the breakpoint IDs 47 ("1", "2") for each breakpoint that was set. 48 """ 49 response = self.dap_server.request_setFunctionBreakpoints( 50 functions, condition=condition, hitCondition=hitCondition 51 ) 52 if response is None: 53 return [] 54 breakpoints = response["body"]["breakpoints"] 55 breakpoint_ids = [] 56 for breakpoint in breakpoints: 57 breakpoint_ids.append("%i" % (breakpoint["id"])) 58 return breakpoint_ids 59 60 def waitUntil(self, condition_callback): 61 for _ in range(20): 62 if condition_callback(): 63 return True 64 time.sleep(0.5) 65 return False 66 67 def verify_breakpoint_hit(self, breakpoint_ids): 68 """Wait for the process we are debugging to stop, and verify we hit 69 any breakpoint location in the "breakpoint_ids" array. 70 "breakpoint_ids" should be a list of breakpoint ID strings 71 (["1", "2"]). The return value from self.set_source_breakpoints() 72 or self.set_function_breakpoints() can be passed to this function""" 73 stopped_events = self.dap_server.wait_for_stopped() 74 for stopped_event in stopped_events: 75 if "body" in stopped_event: 76 body = stopped_event["body"] 77 if "reason" not in body: 78 continue 79 if body["reason"] != "breakpoint": 80 continue 81 if "description" not in body: 82 continue 83 # Descriptions for breakpoints will be in the form 84 # "breakpoint 1.1", so look for any description that matches 85 # ("breakpoint 1.") in the description field as verification 86 # that one of the breakpoint locations was hit. DAP doesn't 87 # allow breakpoints to have multiple locations, but LLDB does. 88 # So when looking at the description we just want to make sure 89 # the right breakpoint matches and not worry about the actual 90 # location. 91 description = body["description"] 92 for breakpoint_id in breakpoint_ids: 93 match_desc = "breakpoint %s." % (breakpoint_id) 94 if match_desc in description: 95 return 96 self.assertTrue(False, "breakpoint not hit") 97 98 def verify_stop_exception_info(self, expected_description): 99 """Wait for the process we are debugging to stop, and verify the stop 100 reason is 'exception' and that the description matches 101 'expected_description' 102 """ 103 stopped_events = self.dap_server.wait_for_stopped() 104 for stopped_event in stopped_events: 105 if "body" in stopped_event: 106 body = stopped_event["body"] 107 if "reason" not in body: 108 continue 109 if body["reason"] != "exception": 110 continue 111 if "description" not in body: 112 continue 113 description = body["description"] 114 if expected_description == description: 115 return True 116 return False 117 118 def verify_commands(self, flavor, output, commands): 119 self.assertTrue(output and len(output) > 0, "expect console output") 120 lines = output.splitlines() 121 prefix = "(lldb) " 122 for cmd in commands: 123 found = False 124 for line in lines: 125 if line.startswith(prefix) and cmd in line: 126 found = True 127 break 128 self.assertTrue( 129 found, "verify '%s' found in console output for '%s'" % (cmd, flavor) 130 ) 131 132 def get_dict_value(self, d, key_path): 133 """Verify each key in the key_path array is in contained in each 134 dictionary within "d". Assert if any key isn't in the 135 corresponding dictionary. This is handy for grabbing values from VS 136 Code response dictionary like getting 137 response['body']['stackFrames'] 138 """ 139 value = d 140 for key in key_path: 141 if key in value: 142 value = value[key] 143 else: 144 self.assertTrue( 145 key in value, 146 'key "%s" from key_path "%s" not in "%s"' % (key, key_path, d), 147 ) 148 return value 149 150 def get_stackFrames_and_totalFramesCount( 151 self, threadId=None, startFrame=None, levels=None, dump=False 152 ): 153 response = self.dap_server.request_stackTrace( 154 threadId=threadId, startFrame=startFrame, levels=levels, dump=dump 155 ) 156 if response: 157 stackFrames = self.get_dict_value(response, ["body", "stackFrames"]) 158 totalFrames = self.get_dict_value(response, ["body", "totalFrames"]) 159 self.assertTrue( 160 totalFrames > 0, 161 "verify totalFrames count is provided by extension that supports " 162 "async frames loading", 163 ) 164 return (stackFrames, totalFrames) 165 return (None, 0) 166 167 def get_stackFrames(self, threadId=None, startFrame=None, levels=None, dump=False): 168 (stackFrames, totalFrames) = self.get_stackFrames_and_totalFramesCount( 169 threadId=threadId, startFrame=startFrame, levels=levels, dump=dump 170 ) 171 return stackFrames 172 173 def get_source_and_line(self, threadId=None, frameIndex=0): 174 stackFrames = self.get_stackFrames( 175 threadId=threadId, startFrame=frameIndex, levels=1 176 ) 177 if stackFrames is not None: 178 stackFrame = stackFrames[0] 179 ["source", "path"] 180 if "source" in stackFrame: 181 source = stackFrame["source"] 182 if "path" in source: 183 if "line" in stackFrame: 184 return (source["path"], stackFrame["line"]) 185 return ("", 0) 186 187 def get_stdout(self, timeout=0.0): 188 return self.dap_server.get_output("stdout", timeout=timeout) 189 190 def get_console(self, timeout=0.0): 191 return self.dap_server.get_output("console", timeout=timeout) 192 193 def collect_console(self, duration): 194 return self.dap_server.collect_output("console", duration=duration) 195 196 def get_local_as_int(self, name, threadId=None): 197 value = self.dap_server.get_local_variable_value(name, threadId=threadId) 198 if value.startswith("0x"): 199 return int(value, 16) 200 elif value.startswith("0"): 201 return int(value, 8) 202 else: 203 return int(value) 204 205 def set_local(self, name, value, id=None): 206 """Set a top level local variable only.""" 207 return self.dap_server.request_setVariable(1, name, str(value), id=id) 208 209 def set_global(self, name, value, id=None): 210 """Set a top level global variable only.""" 211 return self.dap_server.request_setVariable(2, name, str(value), id=id) 212 213 def stepIn(self, threadId=None, waitForStop=True): 214 self.dap_server.request_stepIn(threadId=threadId) 215 if waitForStop: 216 return self.dap_server.wait_for_stopped() 217 return None 218 219 def stepOver(self, threadId=None, waitForStop=True): 220 self.dap_server.request_next(threadId=threadId) 221 if waitForStop: 222 return self.dap_server.wait_for_stopped() 223 return None 224 225 def stepOut(self, threadId=None, waitForStop=True): 226 self.dap_server.request_stepOut(threadId=threadId) 227 if waitForStop: 228 return self.dap_server.wait_for_stopped() 229 return None 230 231 def continue_to_next_stop(self): 232 self.dap_server.request_continue() 233 return self.dap_server.wait_for_stopped() 234 235 def continue_to_breakpoints(self, breakpoint_ids): 236 self.dap_server.request_continue() 237 self.verify_breakpoint_hit(breakpoint_ids) 238 239 def continue_to_exception_breakpoint(self, filter_label): 240 self.dap_server.request_continue() 241 self.assertTrue( 242 self.verify_stop_exception_info(filter_label), 243 'verify we got "%s"' % (filter_label), 244 ) 245 246 def continue_to_exit(self, exitCode=0): 247 self.dap_server.request_continue() 248 stopped_events = self.dap_server.wait_for_stopped() 249 self.assertEquals( 250 len(stopped_events), 1, "stopped_events = {}".format(stopped_events) 251 ) 252 self.assertEquals( 253 stopped_events[0]["event"], "exited", "make sure program ran to completion" 254 ) 255 self.assertEquals( 256 stopped_events[0]["body"]["exitCode"], 257 exitCode, 258 "exitCode == %i" % (exitCode), 259 ) 260 261 def disassemble(self, threadId=None, frameIndex=None): 262 stackFrames = self.get_stackFrames( 263 threadId=threadId, startFrame=frameIndex, levels=1 264 ) 265 self.assertIsNotNone(stackFrames) 266 memoryReference = stackFrames[0]["instructionPointerReference"] 267 self.assertIsNotNone(memoryReference) 268 269 if memoryReference not in self.dap_server.disassembled_instructions: 270 self.dap_server.request_disassemble(memoryReference=memoryReference) 271 272 return self.dap_server.disassembled_instructions[memoryReference] 273 274 def attach( 275 self, 276 program=None, 277 pid=None, 278 waitFor=None, 279 trace=None, 280 initCommands=None, 281 preRunCommands=None, 282 stopCommands=None, 283 exitCommands=None, 284 attachCommands=None, 285 coreFile=None, 286 disconnectAutomatically=True, 287 terminateCommands=None, 288 postRunCommands=None, 289 sourceMap=None, 290 sourceInitFile=False, 291 ): 292 """Build the default Makefile target, create the DAP debug adaptor, 293 and attach to the process. 294 """ 295 296 # Make sure we disconnect and terminate the DAP debug adaptor even 297 # if we throw an exception during the test case. 298 def cleanup(): 299 if disconnectAutomatically: 300 self.dap_server.request_disconnect(terminateDebuggee=True) 301 self.dap_server.terminate() 302 303 # Execute the cleanup function during test case tear down. 304 self.addTearDownHook(cleanup) 305 # Initialize and launch the program 306 self.dap_server.request_initialize(sourceInitFile) 307 response = self.dap_server.request_attach( 308 program=program, 309 pid=pid, 310 waitFor=waitFor, 311 trace=trace, 312 initCommands=initCommands, 313 preRunCommands=preRunCommands, 314 stopCommands=stopCommands, 315 exitCommands=exitCommands, 316 attachCommands=attachCommands, 317 terminateCommands=terminateCommands, 318 coreFile=coreFile, 319 postRunCommands=postRunCommands, 320 sourceMap=sourceMap, 321 ) 322 if not (response and response["success"]): 323 self.assertTrue( 324 response["success"], "attach failed (%s)" % (response["message"]) 325 ) 326 327 def launch( 328 self, 329 program=None, 330 args=None, 331 cwd=None, 332 env=None, 333 stopOnEntry=False, 334 disableASLR=True, 335 disableSTDIO=False, 336 shellExpandArguments=False, 337 trace=False, 338 initCommands=None, 339 preRunCommands=None, 340 stopCommands=None, 341 exitCommands=None, 342 terminateCommands=None, 343 sourcePath=None, 344 debuggerRoot=None, 345 sourceInitFile=False, 346 launchCommands=None, 347 sourceMap=None, 348 disconnectAutomatically=True, 349 runInTerminal=False, 350 expectFailure=False, 351 postRunCommands=None, 352 enableAutoVariableSummaries=False, 353 enableSyntheticChildDebugging=False, 354 commandEscapePrefix="`", 355 customFrameFormat=None, 356 ): 357 """Sending launch request to dap""" 358 359 # Make sure we disconnect and terminate the DAP debug adapter, 360 # if we throw an exception during the test case 361 def cleanup(): 362 if disconnectAutomatically: 363 self.dap_server.request_disconnect(terminateDebuggee=True) 364 self.dap_server.terminate() 365 366 # Execute the cleanup function during test case tear down. 367 self.addTearDownHook(cleanup) 368 369 # Initialize and launch the program 370 self.dap_server.request_initialize(sourceInitFile) 371 response = self.dap_server.request_launch( 372 program, 373 args=args, 374 cwd=cwd, 375 env=env, 376 stopOnEntry=stopOnEntry, 377 disableASLR=disableASLR, 378 disableSTDIO=disableSTDIO, 379 shellExpandArguments=shellExpandArguments, 380 trace=trace, 381 initCommands=initCommands, 382 preRunCommands=preRunCommands, 383 stopCommands=stopCommands, 384 exitCommands=exitCommands, 385 terminateCommands=terminateCommands, 386 sourcePath=sourcePath, 387 debuggerRoot=debuggerRoot, 388 launchCommands=launchCommands, 389 sourceMap=sourceMap, 390 runInTerminal=runInTerminal, 391 postRunCommands=postRunCommands, 392 enableAutoVariableSummaries=enableAutoVariableSummaries, 393 enableSyntheticChildDebugging=enableSyntheticChildDebugging, 394 commandEscapePrefix=commandEscapePrefix, 395 customFrameFormat=customFrameFormat, 396 ) 397 398 if expectFailure: 399 return response 400 401 if not (response and response["success"]): 402 self.assertTrue( 403 response["success"], "launch failed (%s)" % (response["message"]) 404 ) 405 return response 406 407 def build_and_launch( 408 self, 409 program, 410 args=None, 411 cwd=None, 412 env=None, 413 stopOnEntry=False, 414 disableASLR=True, 415 disableSTDIO=False, 416 shellExpandArguments=False, 417 trace=False, 418 initCommands=None, 419 preRunCommands=None, 420 stopCommands=None, 421 exitCommands=None, 422 terminateCommands=None, 423 sourcePath=None, 424 debuggerRoot=None, 425 sourceInitFile=False, 426 runInTerminal=False, 427 disconnectAutomatically=True, 428 postRunCommands=None, 429 lldbDAPEnv=None, 430 enableAutoVariableSummaries=False, 431 enableSyntheticChildDebugging=False, 432 commandEscapePrefix="`", 433 customFrameFormat=None, 434 ): 435 """Build the default Makefile target, create the DAP debug adaptor, 436 and launch the process. 437 """ 438 self.build_and_create_debug_adaptor(lldbDAPEnv) 439 self.assertTrue(os.path.exists(program), "executable must exist") 440 441 return self.launch( 442 program, 443 args, 444 cwd, 445 env, 446 stopOnEntry, 447 disableASLR, 448 disableSTDIO, 449 shellExpandArguments, 450 trace, 451 initCommands, 452 preRunCommands, 453 stopCommands, 454 exitCommands, 455 terminateCommands, 456 sourcePath, 457 debuggerRoot, 458 sourceInitFile, 459 runInTerminal=runInTerminal, 460 disconnectAutomatically=disconnectAutomatically, 461 postRunCommands=postRunCommands, 462 enableAutoVariableSummaries=enableAutoVariableSummaries, 463 enableSyntheticChildDebugging=enableSyntheticChildDebugging, 464 commandEscapePrefix=commandEscapePrefix, 465 customFrameFormat=customFrameFormat, 466 ) 467