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