11364750dSJames Henderson# DExTer : Debugging Experience Tester 21364750dSJames Henderson# ~~~~~~ ~ ~~ ~ ~~ 31364750dSJames Henderson# 41364750dSJames Henderson# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 51364750dSJames Henderson# See https://llvm.org/LICENSE.txt for license information. 61364750dSJames Henderson# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 71364750dSJames Henderson"""Base class for all debugger interface implementations.""" 81364750dSJames Henderson 91364750dSJames Hendersonimport abc 101364750dSJames Hendersonimport os 111364750dSJames Hendersonimport sys 121364750dSJames Hendersonimport traceback 131364750dSJames Hendersonimport unittest 141364750dSJames Henderson 151364750dSJames Hendersonfrom types import SimpleNamespace 167e46a721SStephen Tozerfrom dex.command.CommandBase import StepExpectInfo 171364750dSJames Hendersonfrom dex.dextIR import DebuggerIR, FrameIR, LocIR, StepIR, ValueIR 181364750dSJames Hendersonfrom dex.utils.Exceptions import DebuggerException 191364750dSJames Hendersonfrom dex.utils.ReturnCode import ReturnCode 201364750dSJames Henderson 21*f98ee40fSTobias Hieta 227e46a721SStephen Tozerdef watch_is_active(watch_info: StepExpectInfo, path, frame_idx, line_no): 237e46a721SStephen Tozer _, watch_path, watch_frame_idx, watch_line_range = watch_info 247e46a721SStephen Tozer # If this watch should only be active for a specific file... 257e46a721SStephen Tozer if watch_path and os.path.isfile(watch_path): 267e46a721SStephen Tozer # If the current path does not match the expected file, this watch is 277e46a721SStephen Tozer # not active. 28*f98ee40fSTobias Hieta if not (path and os.path.isfile(path) and os.path.samefile(path, watch_path)): 297e46a721SStephen Tozer return False 307e46a721SStephen Tozer if watch_frame_idx != frame_idx: 317e46a721SStephen Tozer return False 327e46a721SStephen Tozer if watch_line_range and line_no not in list(watch_line_range): 337e46a721SStephen Tozer return False 347e46a721SStephen Tozer return True 351364750dSJames Henderson 36*f98ee40fSTobias Hieta 371364750dSJames Hendersonclass DebuggerBase(object, metaclass=abc.ABCMeta): 381364750dSJames Henderson def __init__(self, context): 391364750dSJames Henderson self.context = context 401364750dSJames Henderson # Note: We can't already read values from options 411364750dSJames Henderson # as DebuggerBase is created before we initialize options 421364750dSJames Henderson # to read potential_debuggers. 431364750dSJames Henderson self.options = self.context.options 441364750dSJames Henderson 451364750dSJames Henderson self._interface = None 461364750dSJames Henderson self.has_loaded = False 4775b31692SStephen Tozer self._loading_error = None 481364750dSJames Henderson try: 491364750dSJames Henderson self._interface = self._load_interface() 501364750dSJames Henderson self.has_loaded = True 511364750dSJames Henderson except DebuggerException: 521364750dSJames Henderson self._loading_error = sys.exc_info() 531364750dSJames Henderson 541364750dSJames Henderson def __enter__(self): 551364750dSJames Henderson try: 561364750dSJames Henderson self._custom_init() 571364750dSJames Henderson self.clear_breakpoints() 581364750dSJames Henderson except DebuggerException: 591364750dSJames Henderson self._loading_error = sys.exc_info() 601364750dSJames Henderson return self 611364750dSJames Henderson 621364750dSJames Henderson def __exit__(self, *args): 631364750dSJames Henderson self._custom_exit() 641364750dSJames Henderson 651364750dSJames Henderson def _custom_init(self): 661364750dSJames Henderson pass 671364750dSJames Henderson 681364750dSJames Henderson def _custom_exit(self): 691364750dSJames Henderson pass 701364750dSJames Henderson 711364750dSJames Henderson @property 721364750dSJames Henderson def debugger_info(self): 731364750dSJames Henderson return DebuggerIR(name=self.name, version=self.version) 741364750dSJames Henderson 751364750dSJames Henderson @property 761364750dSJames Henderson def is_available(self): 771364750dSJames Henderson return self.has_loaded and self.loading_error is None 781364750dSJames Henderson 791364750dSJames Henderson @property 801364750dSJames Henderson def loading_error(self): 81*f98ee40fSTobias Hieta return str(self._loading_error[1]) if self._loading_error is not None else None 821364750dSJames Henderson 831364750dSJames Henderson @property 841364750dSJames Henderson def loading_error_trace(self): 851364750dSJames Henderson if not self._loading_error: 861364750dSJames Henderson return None 871364750dSJames Henderson 881364750dSJames Henderson tb = traceback.format_exception(*self._loading_error) 891364750dSJames Henderson 901364750dSJames Henderson if self._loading_error[1].orig_exception is not None: 911364750dSJames Henderson orig_exception = traceback.format_exception( 92*f98ee40fSTobias Hieta *self._loading_error[1].orig_exception 93*f98ee40fSTobias Hieta ) 941364750dSJames Henderson 95*f98ee40fSTobias Hieta if "".join(orig_exception) not in "".join(tb): 96*f98ee40fSTobias Hieta tb.extend(["\n"]) 971364750dSJames Henderson tb.extend(orig_exception) 981364750dSJames Henderson 99*f98ee40fSTobias Hieta tb = "".join(tb).splitlines(True) 1001364750dSJames Henderson return tb 1011364750dSJames Henderson 1021364750dSJames Henderson def _sanitize_function_name(self, name): # pylint: disable=no-self-use 1031364750dSJames Henderson """If the function name returned by the debugger needs any post- 1041364750dSJames Henderson processing to make it fit (for example, if it includes a byte offset), 1051364750dSJames Henderson do that here. 1061364750dSJames Henderson """ 1071364750dSJames Henderson return name 1081364750dSJames Henderson 1091364750dSJames Henderson @abc.abstractmethod 1101364750dSJames Henderson def _load_interface(self): 1111364750dSJames Henderson pass 1121364750dSJames Henderson 1131364750dSJames Henderson @classmethod 1141364750dSJames Henderson def get_option_name(cls): 1151364750dSJames Henderson """Short name that will be used on the command line to specify this 1161364750dSJames Henderson debugger. 1171364750dSJames Henderson """ 1181364750dSJames Henderson raise NotImplementedError() 1191364750dSJames Henderson 1201364750dSJames Henderson @classmethod 1211364750dSJames Henderson def get_name(cls): 1221364750dSJames Henderson """Full name of this debugger.""" 1231364750dSJames Henderson raise NotImplementedError() 1241364750dSJames Henderson 1251364750dSJames Henderson @property 1261364750dSJames Henderson def name(self): 1271364750dSJames Henderson return self.__class__.get_name() 1281364750dSJames Henderson 1291364750dSJames Henderson @property 1301364750dSJames Henderson def option_name(self): 1311364750dSJames Henderson return self.__class__.get_option_name() 1321364750dSJames Henderson 1331364750dSJames Henderson @abc.abstractproperty 1341364750dSJames Henderson def version(self): 1351364750dSJames Henderson pass 1361364750dSJames Henderson 1371364750dSJames Henderson @abc.abstractmethod 1381364750dSJames Henderson def clear_breakpoints(self): 1391364750dSJames Henderson pass 1401364750dSJames Henderson 1411364750dSJames Henderson def add_breakpoint(self, file_, line): 1421364750dSJames Henderson """Returns a unique opaque breakpoint id. 1431364750dSJames Henderson 1441364750dSJames Henderson The ID type depends on the debugger being used, but will probably be 1451364750dSJames Henderson an int. 1461364750dSJames Henderson """ 1471364750dSJames Henderson return self._add_breakpoint(self._external_to_debug_path(file_), line) 1481364750dSJames Henderson 1491364750dSJames Henderson @abc.abstractmethod 1501364750dSJames Henderson def _add_breakpoint(self, file_, line): 151*f98ee40fSTobias Hieta """Returns a unique opaque breakpoint id.""" 1521364750dSJames Henderson pass 1531364750dSJames Henderson 1541364750dSJames Henderson def add_conditional_breakpoint(self, file_, line, condition): 1551364750dSJames Henderson """Returns a unique opaque breakpoint id. 1561364750dSJames Henderson 1571364750dSJames Henderson The ID type depends on the debugger being used, but will probably be 1581364750dSJames Henderson an int. 1591364750dSJames Henderson """ 1601364750dSJames Henderson return self._add_conditional_breakpoint( 161*f98ee40fSTobias Hieta self._external_to_debug_path(file_), line, condition 162*f98ee40fSTobias Hieta ) 1631364750dSJames Henderson 1641364750dSJames Henderson @abc.abstractmethod 1651364750dSJames Henderson def _add_conditional_breakpoint(self, file_, line, condition): 166*f98ee40fSTobias Hieta """Returns a unique opaque breakpoint id.""" 1671364750dSJames Henderson pass 1681364750dSJames Henderson 1691364750dSJames Henderson @abc.abstractmethod 170b3f14802Sgbtozers def delete_breakpoints(self, ids): 171b3f14802Sgbtozers """Delete a set of breakpoints by ids. 1721364750dSJames Henderson 173b3f14802Sgbtozers Raises a KeyError if, for any id, no breakpoint with that id exists. 1741364750dSJames Henderson """ 1751364750dSJames Henderson pass 1761364750dSJames Henderson 1771364750dSJames Henderson @abc.abstractmethod 1781364750dSJames Henderson def get_triggered_breakpoint_ids(self): 179*f98ee40fSTobias Hieta """Returns a set of opaque ids for just-triggered breakpoints.""" 1801364750dSJames Henderson pass 1811364750dSJames Henderson 1821364750dSJames Henderson @abc.abstractmethod 1831364750dSJames Henderson def launch(self): 1841364750dSJames Henderson pass 1851364750dSJames Henderson 1861364750dSJames Henderson @abc.abstractmethod 1871364750dSJames Henderson def step(self): 1881364750dSJames Henderson pass 1891364750dSJames Henderson 1901364750dSJames Henderson @abc.abstractmethod 1911364750dSJames Henderson def go(self) -> ReturnCode: 1921364750dSJames Henderson pass 1931364750dSJames Henderson 1941364750dSJames Henderson def get_step_info(self, watches, step_index): 1951364750dSJames Henderson step_info = self._get_step_info(watches, step_index) 1961364750dSJames Henderson for frame in step_info.frames: 1971364750dSJames Henderson frame.loc.path = self._debug_to_external_path(frame.loc.path) 1981364750dSJames Henderson return step_info 1991364750dSJames Henderson 2001364750dSJames Henderson @abc.abstractmethod 2011364750dSJames Henderson def _get_step_info(self, watches, step_index): 2021364750dSJames Henderson pass 2031364750dSJames Henderson 2041364750dSJames Henderson @abc.abstractproperty 2051364750dSJames Henderson def is_running(self): 2061364750dSJames Henderson pass 2071364750dSJames Henderson 2081364750dSJames Henderson @abc.abstractproperty 2091364750dSJames Henderson def is_finished(self): 2101364750dSJames Henderson pass 2111364750dSJames Henderson 2121364750dSJames Henderson @abc.abstractproperty 2131364750dSJames Henderson def frames_below_main(self): 2141364750dSJames Henderson pass 2151364750dSJames Henderson 2161364750dSJames Henderson @abc.abstractmethod 2171364750dSJames Henderson def evaluate_expression(self, expression, frame_idx=0) -> ValueIR: 2181364750dSJames Henderson pass 2191364750dSJames Henderson 2201364750dSJames Henderson def _external_to_debug_path(self, path): 2211364750dSJames Henderson if not self.options.debugger_use_relative_paths: 2221364750dSJames Henderson return path 2231364750dSJames Henderson root_dir = self.options.source_root_dir 2241364750dSJames Henderson if not root_dir or not path: 2251364750dSJames Henderson return path 2261364750dSJames Henderson assert path.startswith(root_dir) 2271364750dSJames Henderson return path[len(root_dir) :].lstrip(os.path.sep) 2281364750dSJames Henderson 2291364750dSJames Henderson def _debug_to_external_path(self, path): 2301364750dSJames Henderson if not self.options.debugger_use_relative_paths: 2311364750dSJames Henderson return path 2321364750dSJames Henderson if not path or not self.options.source_root_dir: 2331364750dSJames Henderson return path 2341364750dSJames Henderson for file in self.options.source_files: 2351364750dSJames Henderson if path.endswith(self._external_to_debug_path(file)): 2361364750dSJames Henderson return file 2371364750dSJames Henderson return path 2381364750dSJames Henderson 239*f98ee40fSTobias Hieta 2401364750dSJames Hendersonclass TestDebuggerBase(unittest.TestCase): 2411364750dSJames Henderson class MockDebugger(DebuggerBase): 2421364750dSJames Henderson def __init__(self, context, *args): 2431364750dSJames Henderson super().__init__(context, *args) 2441364750dSJames Henderson self.step_info = None 2451364750dSJames Henderson self.breakpoint_file = None 2461364750dSJames Henderson 2471364750dSJames Henderson def _add_breakpoint(self, file, line): 2481364750dSJames Henderson self.breakpoint_file = file 2491364750dSJames Henderson 2501364750dSJames Henderson def _get_step_info(self, watches, step_index): 2511364750dSJames Henderson return self.step_info 2521364750dSJames Henderson 2531364750dSJames Henderson def __init__(self, *args): 2541364750dSJames Henderson super().__init__(*args) 2551364750dSJames Henderson TestDebuggerBase.MockDebugger.__abstractmethods__ = set() 256*f98ee40fSTobias Hieta self.options = SimpleNamespace(source_root_dir="", source_files=[]) 2571364750dSJames Henderson context = SimpleNamespace(options=self.options) 2581364750dSJames Henderson self.dbg = TestDebuggerBase.MockDebugger(context) 2591364750dSJames Henderson 2601364750dSJames Henderson def _new_step(self, paths): 2611364750dSJames Henderson frames = [ 2621364750dSJames Henderson FrameIR( 2631364750dSJames Henderson function=None, 2641364750dSJames Henderson is_inlined=False, 265*f98ee40fSTobias Hieta loc=LocIR(path=path, lineno=0, column=0), 266*f98ee40fSTobias Hieta ) 267*f98ee40fSTobias Hieta for path in paths 2681364750dSJames Henderson ] 2691364750dSJames Henderson return StepIR(step_index=0, stop_reason=None, frames=frames) 2701364750dSJames Henderson 2711364750dSJames Henderson def _step_paths(self, step): 2721364750dSJames Henderson return [frame.loc.path for frame in step.frames] 2731364750dSJames Henderson 2741364750dSJames Henderson def test_add_breakpoint_no_source_root_dir(self): 2751364750dSJames Henderson self.options.debugger_use_relative_paths = True 276*f98ee40fSTobias Hieta self.options.source_root_dir = "" 277*f98ee40fSTobias Hieta path = os.path.join(os.path.sep + "root", "some_file") 2787c781621STom Weaver self.dbg.add_breakpoint(path, 12) 2797c781621STom Weaver self.assertEqual(path, self.dbg.breakpoint_file) 2801364750dSJames Henderson 2811364750dSJames Henderson def test_add_breakpoint_with_source_root_dir(self): 2821364750dSJames Henderson self.options.debugger_use_relative_paths = True 283*f98ee40fSTobias Hieta self.options.source_root_dir = os.path.sep + "my_root" 284*f98ee40fSTobias Hieta path = os.path.join(self.options.source_root_dir, "some_file") 2857c781621STom Weaver self.dbg.add_breakpoint(path, 12) 286*f98ee40fSTobias Hieta self.assertEqual("some_file", self.dbg.breakpoint_file) 2871364750dSJames Henderson 2881364750dSJames Henderson def test_add_breakpoint_with_source_root_dir_slash_suffix(self): 2891364750dSJames Henderson self.options.debugger_use_relative_paths = True 290*f98ee40fSTobias Hieta self.options.source_root_dir = os.path.sep + "my_root" + os.path.sep 291*f98ee40fSTobias Hieta path = os.path.join(self.options.source_root_dir, "some_file") 2927c781621STom Weaver self.dbg.add_breakpoint(path, 12) 293*f98ee40fSTobias Hieta self.assertEqual("some_file", self.dbg.breakpoint_file) 2941364750dSJames Henderson 2951364750dSJames Henderson def test_get_step_info_no_source_root_dir(self): 2961364750dSJames Henderson self.options.debugger_use_relative_paths = True 297*f98ee40fSTobias Hieta path = os.path.join(os.path.sep + "root", "some_file") 2987c781621STom Weaver self.dbg.step_info = self._new_step([path]) 299*f98ee40fSTobias Hieta self.assertEqual([path], self._step_paths(self.dbg.get_step_info([], 0))) 3001364750dSJames Henderson 3011364750dSJames Henderson def test_get_step_info_no_frames(self): 3021364750dSJames Henderson self.options.debugger_use_relative_paths = True 303*f98ee40fSTobias Hieta self.options.source_root_dir = os.path.sep + "my_root" 3041364750dSJames Henderson self.dbg.step_info = self._new_step([]) 305*f98ee40fSTobias Hieta self.assertEqual([], self._step_paths(self.dbg.get_step_info([], 0))) 3061364750dSJames Henderson 3071364750dSJames Henderson def test_get_step_info(self): 3081364750dSJames Henderson self.options.debugger_use_relative_paths = True 309*f98ee40fSTobias Hieta self.options.source_root_dir = os.path.sep + "my_root" 310*f98ee40fSTobias Hieta path = os.path.join(self.options.source_root_dir, "some_file") 3117c781621STom Weaver self.options.source_files = [path] 312*f98ee40fSTobias Hieta other_path = os.path.join(os.path.sep + "other", "file") 313*f98ee40fSTobias Hieta dbg_path = os.path.join(os.path.sep + "dbg", "some_file") 314*f98ee40fSTobias Hieta self.dbg.step_info = self._new_step([None, other_path, dbg_path]) 315*f98ee40fSTobias Hieta self.assertEqual( 316*f98ee40fSTobias Hieta [None, other_path, path], self._step_paths(self.dbg.get_step_info([], 0)) 317*f98ee40fSTobias Hieta ) 318