You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
270 lines
8.2 KiB
270 lines
8.2 KiB
# DExTer : Debugging Experience Tester
|
|
# ~~~~~~ ~ ~~ ~ ~~
|
|
#
|
|
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
# See https://llvm.org/LICENSE.txt for license information.
|
|
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
"""Base class for all debugger interface implementations."""
|
|
|
|
import abc
|
|
import os
|
|
import sys
|
|
import traceback
|
|
import unittest
|
|
|
|
from types import SimpleNamespace
|
|
from dex.dextIR import DebuggerIR, FrameIR, LocIR, StepIR, ValueIR
|
|
from dex.utils.Exceptions import DebuggerException
|
|
from dex.utils.Exceptions import NotYetLoadedDebuggerException
|
|
from dex.utils.ReturnCode import ReturnCode
|
|
|
|
|
|
class DebuggerBase(object, metaclass=abc.ABCMeta):
|
|
def __init__(self, context):
|
|
self.context = context
|
|
# Note: We can't already read values from options
|
|
# as DebuggerBase is created before we initialize options
|
|
# to read potential_debuggers.
|
|
self.options = self.context.options
|
|
|
|
self._interface = None
|
|
self.has_loaded = False
|
|
self._loading_error = NotYetLoadedDebuggerException()
|
|
try:
|
|
self._interface = self._load_interface()
|
|
self.has_loaded = True
|
|
self._loading_error = None
|
|
except DebuggerException:
|
|
self._loading_error = sys.exc_info()
|
|
|
|
def __enter__(self):
|
|
try:
|
|
self._custom_init()
|
|
self.clear_breakpoints()
|
|
except DebuggerException:
|
|
self._loading_error = sys.exc_info()
|
|
return self
|
|
|
|
def __exit__(self, *args):
|
|
self._custom_exit()
|
|
|
|
def _custom_init(self):
|
|
pass
|
|
|
|
def _custom_exit(self):
|
|
pass
|
|
|
|
@property
|
|
def debugger_info(self):
|
|
return DebuggerIR(name=self.name, version=self.version)
|
|
|
|
@property
|
|
def is_available(self):
|
|
return self.has_loaded and self.loading_error is None
|
|
|
|
@property
|
|
def loading_error(self):
|
|
return (str(self._loading_error[1])
|
|
if self._loading_error is not None else None)
|
|
|
|
@property
|
|
def loading_error_trace(self):
|
|
if not self._loading_error:
|
|
return None
|
|
|
|
tb = traceback.format_exception(*self._loading_error)
|
|
|
|
if self._loading_error[1].orig_exception is not None:
|
|
orig_exception = traceback.format_exception(
|
|
*self._loading_error[1].orig_exception)
|
|
|
|
if ''.join(orig_exception) not in ''.join(tb):
|
|
tb.extend(['\n'])
|
|
tb.extend(orig_exception)
|
|
|
|
tb = ''.join(tb).splitlines(True)
|
|
return tb
|
|
|
|
def _sanitize_function_name(self, name): # pylint: disable=no-self-use
|
|
"""If the function name returned by the debugger needs any post-
|
|
processing to make it fit (for example, if it includes a byte offset),
|
|
do that here.
|
|
"""
|
|
return name
|
|
|
|
@abc.abstractmethod
|
|
def _load_interface(self):
|
|
pass
|
|
|
|
@classmethod
|
|
def get_option_name(cls):
|
|
"""Short name that will be used on the command line to specify this
|
|
debugger.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
@classmethod
|
|
def get_name(cls):
|
|
"""Full name of this debugger."""
|
|
raise NotImplementedError()
|
|
|
|
@property
|
|
def name(self):
|
|
return self.__class__.get_name()
|
|
|
|
@property
|
|
def option_name(self):
|
|
return self.__class__.get_option_name()
|
|
|
|
@abc.abstractproperty
|
|
def version(self):
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def clear_breakpoints(self):
|
|
pass
|
|
|
|
def add_breakpoint(self, file_, line):
|
|
return self._add_breakpoint(self._external_to_debug_path(file_), line)
|
|
|
|
@abc.abstractmethod
|
|
def _add_breakpoint(self, file_, line):
|
|
pass
|
|
|
|
def add_conditional_breakpoint(self, file_, line, condition):
|
|
return self._add_conditional_breakpoint(
|
|
self._external_to_debug_path(file_), line, condition)
|
|
|
|
@abc.abstractmethod
|
|
def _add_conditional_breakpoint(self, file_, line, condition):
|
|
pass
|
|
|
|
def delete_conditional_breakpoint(self, file_, line, condition):
|
|
return self._delete_conditional_breakpoint(
|
|
self._external_to_debug_path(file_), line, condition)
|
|
|
|
@abc.abstractmethod
|
|
def _delete_conditional_breakpoint(self, file_, line, condition):
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def launch(self):
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def step(self):
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def go(self) -> ReturnCode:
|
|
pass
|
|
|
|
def get_step_info(self, watches, step_index):
|
|
step_info = self._get_step_info(watches, step_index)
|
|
for frame in step_info.frames:
|
|
frame.loc.path = self._debug_to_external_path(frame.loc.path)
|
|
return step_info
|
|
|
|
@abc.abstractmethod
|
|
def _get_step_info(self, watches, step_index):
|
|
pass
|
|
|
|
@abc.abstractproperty
|
|
def is_running(self):
|
|
pass
|
|
|
|
@abc.abstractproperty
|
|
def is_finished(self):
|
|
pass
|
|
|
|
@abc.abstractproperty
|
|
def frames_below_main(self):
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def evaluate_expression(self, expression, frame_idx=0) -> ValueIR:
|
|
pass
|
|
|
|
def _external_to_debug_path(self, path):
|
|
root_dir = self.options.source_root_dir
|
|
if not root_dir or not path:
|
|
return path
|
|
assert path.startswith(root_dir)
|
|
return path[len(root_dir):].lstrip(os.path.sep)
|
|
|
|
def _debug_to_external_path(self, path):
|
|
if not path or not self.options.source_root_dir:
|
|
return path
|
|
for file in self.options.source_files:
|
|
if path.endswith(self._external_to_debug_path(file)):
|
|
return file
|
|
return path
|
|
|
|
class TestDebuggerBase(unittest.TestCase):
|
|
|
|
class MockDebugger(DebuggerBase):
|
|
|
|
def __init__(self, context, *args):
|
|
super().__init__(context, *args)
|
|
self.step_info = None
|
|
self.breakpoint_file = None
|
|
|
|
def _add_breakpoint(self, file, line):
|
|
self.breakpoint_file = file
|
|
|
|
def _get_step_info(self, watches, step_index):
|
|
return self.step_info
|
|
|
|
def __init__(self, *args):
|
|
super().__init__(*args)
|
|
TestDebuggerBase.MockDebugger.__abstractmethods__ = set()
|
|
self.options = SimpleNamespace(source_root_dir = '', source_files = [])
|
|
context = SimpleNamespace(options = self.options)
|
|
self.dbg = TestDebuggerBase.MockDebugger(context)
|
|
|
|
def _new_step(self, paths):
|
|
frames = [
|
|
FrameIR(
|
|
function=None,
|
|
is_inlined=False,
|
|
loc=LocIR(path=path, lineno=0, column=0)) for path in paths
|
|
]
|
|
return StepIR(step_index=0, stop_reason=None, frames=frames)
|
|
|
|
def _step_paths(self, step):
|
|
return [frame.loc.path for frame in step.frames]
|
|
|
|
def test_add_breakpoint_no_source_root_dir(self):
|
|
self.options.source_root_dir = ''
|
|
self.dbg.add_breakpoint('/root/some_file', 12)
|
|
self.assertEqual('/root/some_file', self.dbg.breakpoint_file)
|
|
|
|
def test_add_breakpoint_with_source_root_dir(self):
|
|
self.options.source_root_dir = '/my_root'
|
|
self.dbg.add_breakpoint('/my_root/some_file', 12)
|
|
self.assertEqual('some_file', self.dbg.breakpoint_file)
|
|
|
|
def test_add_breakpoint_with_source_root_dir_slash_suffix(self):
|
|
self.options.source_root_dir = '/my_root/'
|
|
self.dbg.add_breakpoint('/my_root/some_file', 12)
|
|
self.assertEqual('some_file', self.dbg.breakpoint_file)
|
|
|
|
def test_get_step_info_no_source_root_dir(self):
|
|
self.dbg.step_info = self._new_step(['/root/some_file'])
|
|
self.assertEqual(['/root/some_file'],
|
|
self._step_paths(self.dbg.get_step_info([], 0)))
|
|
|
|
def test_get_step_info_no_frames(self):
|
|
self.options.source_root_dir = '/my_root'
|
|
self.dbg.step_info = self._new_step([])
|
|
self.assertEqual([],
|
|
self._step_paths(self.dbg.get_step_info([], 0)))
|
|
|
|
def test_get_step_info(self):
|
|
self.options.source_root_dir = '/my_root'
|
|
self.options.source_files = ['/my_root/some_file']
|
|
self.dbg.step_info = self._new_step(
|
|
[None, '/other/file', '/dbg/some_file'])
|
|
self.assertEqual([None, '/other/file', '/my_root/some_file'],
|
|
self._step_paths(self.dbg.get_step_info([], 0)))
|