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