brintos

brintos / llvm-project-archived public Read only

0
0
Text · 11.0 KiB · f8bee4e Raw
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