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 return self._add_breakpoint(self._external_to_debug_path(file_), line) 129 130 @abc.abstractmethod 131 def _add_breakpoint(self, file_, line): 132 pass 133 134 def add_conditional_breakpoint(self, file_, line, condition): 135 return self._add_conditional_breakpoint( 136 self._external_to_debug_path(file_), line, condition) 137 138 @abc.abstractmethod 139 def _add_conditional_breakpoint(self, file_, line, condition): 140 pass 141 142 def delete_conditional_breakpoint(self, file_, line, condition): 143 return self._delete_conditional_breakpoint( 144 self._external_to_debug_path(file_), line, condition) 145 146 @abc.abstractmethod 147 def _delete_conditional_breakpoint(self, file_, line, condition): 148 pass 149 150 @abc.abstractmethod 151 def launch(self): 152 pass 153 154 @abc.abstractmethod 155 def step(self): 156 pass 157 158 @abc.abstractmethod 159 def go(self) -> ReturnCode: 160 pass 161 162 def get_step_info(self, watches, step_index): 163 step_info = self._get_step_info(watches, step_index) 164 for frame in step_info.frames: 165 frame.loc.path = self._debug_to_external_path(frame.loc.path) 166 return step_info 167 168 @abc.abstractmethod 169 def _get_step_info(self, watches, step_index): 170 pass 171 172 @abc.abstractproperty 173 def is_running(self): 174 pass 175 176 @abc.abstractproperty 177 def is_finished(self): 178 pass 179 180 @abc.abstractproperty 181 def frames_below_main(self): 182 pass 183 184 @abc.abstractmethod 185 def evaluate_expression(self, expression, frame_idx=0) -> ValueIR: 186 pass 187 188 def _external_to_debug_path(self, path): 189 root_dir = self.options.source_root_dir 190 if not root_dir or not path: 191 return path 192 assert path.startswith(root_dir) 193 return path[len(root_dir):].lstrip(os.path.sep) 194 195 def _debug_to_external_path(self, path): 196 if not path or not self.options.source_root_dir: 197 return path 198 for file in self.options.source_files: 199 if path.endswith(self._external_to_debug_path(file)): 200 return file 201 return path 202 203class TestDebuggerBase(unittest.TestCase): 204 205 class MockDebugger(DebuggerBase): 206 207 def __init__(self, context, *args): 208 super().__init__(context, *args) 209 self.step_info = None 210 self.breakpoint_file = None 211 212 def _add_breakpoint(self, file, line): 213 self.breakpoint_file = file 214 215 def _get_step_info(self, watches, step_index): 216 return self.step_info 217 218 def __init__(self, *args): 219 super().__init__(*args) 220 TestDebuggerBase.MockDebugger.__abstractmethods__ = set() 221 self.options = SimpleNamespace(source_root_dir = '', source_files = []) 222 context = SimpleNamespace(options = self.options) 223 self.dbg = TestDebuggerBase.MockDebugger(context) 224 225 def _new_step(self, paths): 226 frames = [ 227 FrameIR( 228 function=None, 229 is_inlined=False, 230 loc=LocIR(path=path, lineno=0, column=0)) for path in paths 231 ] 232 return StepIR(step_index=0, stop_reason=None, frames=frames) 233 234 def _step_paths(self, step): 235 return [frame.loc.path for frame in step.frames] 236 237 def test_add_breakpoint_no_source_root_dir(self): 238 self.options.source_root_dir = '' 239 self.dbg.add_breakpoint('/root/some_file', 12) 240 self.assertEqual('/root/some_file', self.dbg.breakpoint_file) 241 242 def test_add_breakpoint_with_source_root_dir(self): 243 self.options.source_root_dir = '/my_root' 244 self.dbg.add_breakpoint('/my_root/some_file', 12) 245 self.assertEqual('some_file', self.dbg.breakpoint_file) 246 247 def test_add_breakpoint_with_source_root_dir_slash_suffix(self): 248 self.options.source_root_dir = '/my_root/' 249 self.dbg.add_breakpoint('/my_root/some_file', 12) 250 self.assertEqual('some_file', self.dbg.breakpoint_file) 251 252 def test_get_step_info_no_source_root_dir(self): 253 self.dbg.step_info = self._new_step(['/root/some_file']) 254 self.assertEqual(['/root/some_file'], 255 self._step_paths(self.dbg.get_step_info([], 0))) 256 257 def test_get_step_info_no_frames(self): 258 self.options.source_root_dir = '/my_root' 259 self.dbg.step_info = self._new_step([]) 260 self.assertEqual([], 261 self._step_paths(self.dbg.get_step_info([], 0))) 262 263 def test_get_step_info(self): 264 self.options.source_root_dir = '/my_root' 265 self.options.source_files = ['/my_root/some_file'] 266 self.dbg.step_info = self._new_step( 267 [None, '/other/file', '/dbg/some_file']) 268 self.assertEqual([None, '/other/file', '/my_root/some_file'], 269 self._step_paths(self.dbg.get_step_info([], 0))) 270