345 lines · python
1# DExTer : Debugging Experience Tester2# ~~~~~~ ~ ~~ ~ ~~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-exception7"""Base class for all debugger interface implementations."""8 9import abc10import os11import sys12import traceback13import unittest14 15from types import SimpleNamespace16from dex.command.CommandBase import StepExpectInfo17from dex.dextIR import DebuggerIR, FrameIR, LocIR, StepIR, ValueIR18from dex.utils.Exceptions import DebuggerException19from dex.utils.ReturnCode import ReturnCode20 21 22def watch_is_active(watch_info: StepExpectInfo, path, frame_idx, line_no):23 _, watch_path, watch_frame_idx, watch_line_range = watch_info24 # 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 is27 # not active.28 if not (path and os.path.isfile(path) and os.path.samefile(path, watch_path)):29 return False30 if watch_frame_idx != frame_idx:31 return False32 if watch_line_range and line_no not in list(watch_line_range):33 return False34 return True35 36 37class DebuggerBase(object, metaclass=abc.ABCMeta):38 def __init__(self, context):39 self.context = context40 # Note: We can't already read values from options41 # as DebuggerBase is created before we initialize options42 # to read potential_debuggers.43 self.options = self.context.options44 45 self._interface = None46 self.has_loaded = False47 self._loading_error = None48 try:49 self._interface = self._load_interface()50 self.has_loaded = True51 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 self61 62 def __exit__(self, *args):63 self._custom_exit()64 65 def _custom_init(self):66 pass67 68 def _custom_exit(self):69 pass70 71 @property72 def debugger_info(self):73 return DebuggerIR(name=self.name, version=self.version)74 75 @property76 def is_available(self):77 return self.has_loaded and self.loading_error is None78 79 @property80 def loading_error(self):81 return str(self._loading_error[1]) if self._loading_error is not None else None82 83 @property84 def loading_error_trace(self):85 if not self._loading_error:86 return None87 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_exception93 )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 tb101 102 def _sanitize_function_name(self, name): # pylint: disable=no-self-use103 """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 name108 109 @abc.abstractmethod110 def _load_interface(self):111 pass112 113 @classmethod114 def get_option_name(cls):115 """Short name that will be used on the command line to specify this116 debugger.117 """118 raise NotImplementedError()119 120 @classmethod121 def get_name(cls):122 """Full name of this debugger."""123 raise NotImplementedError()124 125 @property126 def name(self):127 return self.__class__.get_name()128 129 @property130 def option_name(self):131 return self.__class__.get_option_name()132 133 @abc.abstractproperty134 def version(self):135 pass136 137 @abc.abstractmethod138 def clear_breakpoints(self):139 pass140 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 be145 an int.146 """147 return self._add_breakpoint(self._external_to_debug_path(file_), line)148 149 @abc.abstractmethod150 def _add_breakpoint(self, file_, line):151 """Returns a unique opaque breakpoint id."""152 pass153 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 be158 an int.159 """160 return self._add_conditional_breakpoint(161 self._external_to_debug_path(file_), line, condition162 )163 164 @abc.abstractmethod165 def _add_conditional_breakpoint(self, file_, line, condition):166 """Returns a unique opaque breakpoint id."""167 pass168 169 def add_function_breakpoint(self, name):170 """Returns a unique opaque breakpoint id.171 172 The ID type depends on the debugger being used, but will probably be173 an int.174 """175 raise NotImplementedError()176 177 def add_instruction_breakpoint(self, addr):178 """Returns a unique opaque breakpoint id.179 180 The ID type depends on the debugger being used, but will probably be181 an int.182 """183 raise NotImplementedError()184 185 @abc.abstractmethod186 def delete_breakpoints(self, ids):187 """Delete a set of breakpoints by ids.188 189 Raises a KeyError if, for any id, no breakpoint with that id exists.190 """191 pass192 193 @abc.abstractmethod194 def get_triggered_breakpoint_ids(self):195 """Returns a set of opaque ids for just-triggered breakpoints."""196 pass197 198 @abc.abstractmethod199 def launch(self):200 pass201 202 @abc.abstractmethod203 def step_in(self):204 pass205 206 @abc.abstractmethod207 def go(self) -> ReturnCode:208 pass209 210 def get_step_info(self, watches, step_index):211 step_info = self._get_step_info(watches, step_index)212 for frame in step_info.frames:213 frame.loc.path = self._debug_to_external_path(frame.loc.path)214 return step_info215 216 @abc.abstractmethod217 def _get_step_info(self, watches, step_index):218 pass219 220 @abc.abstractproperty221 def is_running(self):222 pass223 224 @abc.abstractproperty225 def is_finished(self):226 pass227 228 @abc.abstractproperty229 def frames_below_main(self):230 pass231 232 @abc.abstractmethod233 def evaluate_expression(self, expression, frame_idx=0) -> ValueIR:234 pass235 236 def get_pc(self, frame_idx: int = 0) -> str:237 """Get the current PC in frame at frame_idx depth.238 frame_idx 0 is the current function.239 """240 r = self.evaluate_expression("$pc", frame_idx)241 if not r.could_evaluate or r.is_optimized_away or r.is_irretrievable:242 raise DebuggerException(243 "evaluating '$pc' failed - possibly unsupported by the debugger"244 )245 return r.value246 247 def _external_to_debug_path(self, path):248 if not self.options.debugger_use_relative_paths:249 return path250 root_dir = self.options.source_root_dir251 if not root_dir or not path:252 return path253 assert path.startswith(root_dir)254 return path[len(root_dir) :].lstrip(os.path.sep)255 256 def _debug_to_external_path(self, path):257 if not self.options.debugger_use_relative_paths:258 return path259 if not path or not self.options.source_root_dir:260 return path261 for file in self.options.source_files:262 if path.endswith(self._external_to_debug_path(file)):263 return file264 return path265 266 267class TestDebuggerBase(unittest.TestCase):268 class MockDebugger(DebuggerBase):269 def __init__(self, context, *args):270 super().__init__(context, *args)271 self.step_info = None272 self.breakpoint_file = None273 274 def _add_breakpoint(self, file, line):275 self.breakpoint_file = file276 277 def _get_step_info(self, watches, step_index):278 return self.step_info279 280 def __init__(self, *args):281 super().__init__(*args)282 TestDebuggerBase.MockDebugger.__abstractmethods__ = set()283 self.options = SimpleNamespace(source_root_dir="", source_files=[])284 context = SimpleNamespace(options=self.options)285 self.dbg = TestDebuggerBase.MockDebugger(context)286 287 def _new_step(self, paths):288 frames = [289 FrameIR(290 function=None,291 is_inlined=False,292 loc=LocIR(path=path, lineno=0, column=0),293 )294 for path in paths295 ]296 return StepIR(step_index=0, stop_reason=None, frames=frames)297 298 def _step_paths(self, step):299 return [frame.loc.path for frame in step.frames]300 301 def test_add_breakpoint_no_source_root_dir(self):302 self.options.debugger_use_relative_paths = True303 self.options.source_root_dir = ""304 path = os.path.join(os.path.sep + "root", "some_file")305 self.dbg.add_breakpoint(path, 12)306 self.assertEqual(path, self.dbg.breakpoint_file)307 308 def test_add_breakpoint_with_source_root_dir(self):309 self.options.debugger_use_relative_paths = True310 self.options.source_root_dir = os.path.sep + "my_root"311 path = os.path.join(self.options.source_root_dir, "some_file")312 self.dbg.add_breakpoint(path, 12)313 self.assertEqual("some_file", self.dbg.breakpoint_file)314 315 def test_add_breakpoint_with_source_root_dir_slash_suffix(self):316 self.options.debugger_use_relative_paths = True317 self.options.source_root_dir = os.path.sep + "my_root" + os.path.sep318 path = os.path.join(self.options.source_root_dir, "some_file")319 self.dbg.add_breakpoint(path, 12)320 self.assertEqual("some_file", self.dbg.breakpoint_file)321 322 def test_get_step_info_no_source_root_dir(self):323 self.options.debugger_use_relative_paths = True324 path = os.path.join(os.path.sep + "root", "some_file")325 self.dbg.step_info = self._new_step([path])326 self.assertEqual([path], self._step_paths(self.dbg.get_step_info([], 0)))327 328 def test_get_step_info_no_frames(self):329 self.options.debugger_use_relative_paths = True330 self.options.source_root_dir = os.path.sep + "my_root"331 self.dbg.step_info = self._new_step([])332 self.assertEqual([], self._step_paths(self.dbg.get_step_info([], 0)))333 334 def test_get_step_info(self):335 self.options.debugger_use_relative_paths = True336 self.options.source_root_dir = os.path.sep + "my_root"337 path = os.path.join(self.options.source_root_dir, "some_file")338 self.options.source_files = [path]339 other_path = os.path.join(os.path.sep + "other", "file")340 dbg_path = os.path.join(os.path.sep + "dbg", "some_file")341 self.dbg.step_info = self._new_step([None, other_path, dbg_path])342 self.assertEqual(343 [None, other_path, path], self._step_paths(self.dbg.get_step_info([], 0))344 )345