1# DExTer : Debugging Experience Tester 2# ~~~~~~ ~ ~~ ~ ~~ 3# 4# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 5# See https://llvm.org/LICENSE.txt for license information. 6# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 7"""Base class for all debugger interface implementations.""" 8 9import abc 10import os 11import sys 12import traceback 13import unittest 14 15from types import SimpleNamespace 16from dex.command.CommandBase import StepExpectInfo 17from dex.dextIR import DebuggerIR, FrameIR, LocIR, StepIR, ValueIR 18from dex.utils.Exceptions import DebuggerException 19from dex.utils.ReturnCode import ReturnCode 20 21 22def watch_is_active(watch_info: StepExpectInfo, path, frame_idx, line_no): 23 _, watch_path, watch_frame_idx, watch_line_range = watch_info 24 # If this watch should only be active for a specific file... 25 if watch_path and os.path.isfile(watch_path): 26 # If the current path does not match the expected file, this watch is 27 # not active. 28 if not (path and os.path.isfile(path) and os.path.samefile(path, watch_path)): 29 return False 30 if watch_frame_idx != frame_idx: 31 return False 32 if watch_line_range and line_no not in list(watch_line_range): 33 return False 34 return True 35 36 37class DebuggerBase(object, metaclass=abc.ABCMeta): 38 def __init__(self, context): 39 self.context = context 40 # Note: We can't already read values from options 41 # as DebuggerBase is created before we initialize options 42 # to read potential_debuggers. 43 self.options = self.context.options 44 45 self._interface = None 46 self.has_loaded = False 47 self._loading_error = None 48 try: 49 self._interface = self._load_interface() 50 self.has_loaded = True 51 except DebuggerException: 52 self._loading_error = sys.exc_info() 53 54 def __enter__(self): 55 try: 56 self._custom_init() 57 self.clear_breakpoints() 58 except DebuggerException: 59 self._loading_error = sys.exc_info() 60 return self 61 62 def __exit__(self, *args): 63 self._custom_exit() 64 65 def _custom_init(self): 66 pass 67 68 def _custom_exit(self): 69 pass 70 71 @property 72 def debugger_info(self): 73 return DebuggerIR(name=self.name, version=self.version) 74 75 @property 76 def is_available(self): 77 return self.has_loaded and self.loading_error is None 78 79 @property 80 def loading_error(self): 81 return str(self._loading_error[1]) if self._loading_error is not None else None 82 83 @property 84 def loading_error_trace(self): 85 if not self._loading_error: 86 return None 87 88 tb = traceback.format_exception(*self._loading_error) 89 90 if self._loading_error[1].orig_exception is not None: 91 orig_exception = traceback.format_exception( 92 *self._loading_error[1].orig_exception 93 ) 94 95 if "".join(orig_exception) not in "".join(tb): 96 tb.extend(["\n"]) 97 tb.extend(orig_exception) 98 99 tb = "".join(tb).splitlines(True) 100 return tb 101 102 def _sanitize_function_name(self, name): # pylint: disable=no-self-use 103 """If the function name returned by the debugger needs any post- 104 processing to make it fit (for example, if it includes a byte offset), 105 do that here. 106 """ 107 return name 108 109 @abc.abstractmethod 110 def _load_interface(self): 111 pass 112 113 @classmethod 114 def get_option_name(cls): 115 """Short name that will be used on the command line to specify this 116 debugger. 117 """ 118 raise NotImplementedError() 119 120 @classmethod 121 def get_name(cls): 122 """Full name of this debugger.""" 123 raise NotImplementedError() 124 125 @property 126 def name(self): 127 return self.__class__.get_name() 128 129 @property 130 def option_name(self): 131 return self.__class__.get_option_name() 132 133 @abc.abstractproperty 134 def version(self): 135 pass 136 137 @abc.abstractmethod 138 def clear_breakpoints(self): 139 pass 140 141 def add_breakpoint(self, file_, line): 142 """Returns a unique opaque breakpoint id. 143 144 The ID type depends on the debugger being used, but will probably be 145 an int. 146 """ 147 return self._add_breakpoint(self._external_to_debug_path(file_), line) 148 149 @abc.abstractmethod 150 def _add_breakpoint(self, file_, line): 151 """Returns a unique opaque breakpoint id.""" 152 pass 153 154 def add_conditional_breakpoint(self, file_, line, condition): 155 """Returns a unique opaque breakpoint id. 156 157 The ID type depends on the debugger being used, but will probably be 158 an int. 159 """ 160 return self._add_conditional_breakpoint( 161 self._external_to_debug_path(file_), line, condition 162 ) 163 164 @abc.abstractmethod 165 def _add_conditional_breakpoint(self, file_, line, condition): 166 """Returns a unique opaque breakpoint id.""" 167 pass 168 169 @abc.abstractmethod 170 def delete_breakpoints(self, ids): 171 """Delete a set of breakpoints by ids. 172 173 Raises a KeyError if, for any id, no breakpoint with that id exists. 174 """ 175 pass 176 177 @abc.abstractmethod 178 def get_triggered_breakpoint_ids(self): 179 """Returns a set of opaque ids for just-triggered breakpoints.""" 180 pass 181 182 @abc.abstractmethod 183 def launch(self): 184 pass 185 186 @abc.abstractmethod 187 def step(self): 188 pass 189 190 @abc.abstractmethod 191 def go(self) -> ReturnCode: 192 pass 193 194 def get_step_info(self, watches, step_index): 195 step_info = self._get_step_info(watches, step_index) 196 for frame in step_info.frames: 197 frame.loc.path = self._debug_to_external_path(frame.loc.path) 198 return step_info 199 200 @abc.abstractmethod 201 def _get_step_info(self, watches, step_index): 202 pass 203 204 @abc.abstractproperty 205 def is_running(self): 206 pass 207 208 @abc.abstractproperty 209 def is_finished(self): 210 pass 211 212 @abc.abstractproperty 213 def frames_below_main(self): 214 pass 215 216 @abc.abstractmethod 217 def evaluate_expression(self, expression, frame_idx=0) -> ValueIR: 218 pass 219 220 def _external_to_debug_path(self, path): 221 if not self.options.debugger_use_relative_paths: 222 return path 223 root_dir = self.options.source_root_dir 224 if not root_dir or not path: 225 return path 226 assert path.startswith(root_dir) 227 return path[len(root_dir) :].lstrip(os.path.sep) 228 229 def _debug_to_external_path(self, path): 230 if not self.options.debugger_use_relative_paths: 231 return path 232 if not path or not self.options.source_root_dir: 233 return path 234 for file in self.options.source_files: 235 if path.endswith(self._external_to_debug_path(file)): 236 return file 237 return path 238 239 240class TestDebuggerBase(unittest.TestCase): 241 class MockDebugger(DebuggerBase): 242 def __init__(self, context, *args): 243 super().__init__(context, *args) 244 self.step_info = None 245 self.breakpoint_file = None 246 247 def _add_breakpoint(self, file, line): 248 self.breakpoint_file = file 249 250 def _get_step_info(self, watches, step_index): 251 return self.step_info 252 253 def __init__(self, *args): 254 super().__init__(*args) 255 TestDebuggerBase.MockDebugger.__abstractmethods__ = set() 256 self.options = SimpleNamespace(source_root_dir="", source_files=[]) 257 context = SimpleNamespace(options=self.options) 258 self.dbg = TestDebuggerBase.MockDebugger(context) 259 260 def _new_step(self, paths): 261 frames = [ 262 FrameIR( 263 function=None, 264 is_inlined=False, 265 loc=LocIR(path=path, lineno=0, column=0), 266 ) 267 for path in paths 268 ] 269 return StepIR(step_index=0, stop_reason=None, frames=frames) 270 271 def _step_paths(self, step): 272 return [frame.loc.path for frame in step.frames] 273 274 def test_add_breakpoint_no_source_root_dir(self): 275 self.options.debugger_use_relative_paths = True 276 self.options.source_root_dir = "" 277 path = os.path.join(os.path.sep + "root", "some_file") 278 self.dbg.add_breakpoint(path, 12) 279 self.assertEqual(path, self.dbg.breakpoint_file) 280 281 def test_add_breakpoint_with_source_root_dir(self): 282 self.options.debugger_use_relative_paths = True 283 self.options.source_root_dir = os.path.sep + "my_root" 284 path = os.path.join(self.options.source_root_dir, "some_file") 285 self.dbg.add_breakpoint(path, 12) 286 self.assertEqual("some_file", self.dbg.breakpoint_file) 287 288 def test_add_breakpoint_with_source_root_dir_slash_suffix(self): 289 self.options.debugger_use_relative_paths = True 290 self.options.source_root_dir = os.path.sep + "my_root" + os.path.sep 291 path = os.path.join(self.options.source_root_dir, "some_file") 292 self.dbg.add_breakpoint(path, 12) 293 self.assertEqual("some_file", self.dbg.breakpoint_file) 294 295 def test_get_step_info_no_source_root_dir(self): 296 self.options.debugger_use_relative_paths = True 297 path = os.path.join(os.path.sep + "root", "some_file") 298 self.dbg.step_info = self._new_step([path]) 299 self.assertEqual([path], self._step_paths(self.dbg.get_step_info([], 0))) 300 301 def test_get_step_info_no_frames(self): 302 self.options.debugger_use_relative_paths = True 303 self.options.source_root_dir = os.path.sep + "my_root" 304 self.dbg.step_info = self._new_step([]) 305 self.assertEqual([], self._step_paths(self.dbg.get_step_info([], 0))) 306 307 def test_get_step_info(self): 308 self.options.debugger_use_relative_paths = True 309 self.options.source_root_dir = os.path.sep + "my_root" 310 path = os.path.join(self.options.source_root_dir, "some_file") 311 self.options.source_files = [path] 312 other_path = os.path.join(os.path.sep + "other", "file") 313 dbg_path = os.path.join(os.path.sep + "dbg", "some_file") 314 self.dbg.step_info = self._new_step([None, other_path, dbg_path]) 315 self.assertEqual( 316 [None, other_path, path], self._step_paths(self.dbg.get_step_info([], 0)) 317 ) 318