xref: /llvm-project/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DebuggerBase.py (revision f98ee40f4b5d7474fc67e82824bf6abbaedb7b1c)
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.command.CommandBase import StepExpectInfo
17from dex.dextIR import DebuggerIR, FrameIR, LocIR, StepIR, ValueIR
18from dex.utils.Exceptions import DebuggerException
19from dex.utils.ReturnCode import ReturnCode
20
21
22def watch_is_active(watch_info: StepExpectInfo, path, frame_idx, line_no):
23    _, watch_path, watch_frame_idx, watch_line_range = watch_info
24    # 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 is
27        # not active.
28        if not (path and os.path.isfile(path) and os.path.samefile(path, watch_path)):
29            return False
30    if watch_frame_idx != frame_idx:
31        return False
32    if watch_line_range and line_no not in list(watch_line_range):
33        return False
34    return True
35
36
37class DebuggerBase(object, metaclass=abc.ABCMeta):
38    def __init__(self, context):
39        self.context = context
40        # Note: We can't already read values from options
41        # as DebuggerBase is created before we initialize options
42        # to read potential_debuggers.
43        self.options = self.context.options
44
45        self._interface = None
46        self.has_loaded = False
47        self._loading_error = None
48        try:
49            self._interface = self._load_interface()
50            self.has_loaded = True
51        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 self
61
62    def __exit__(self, *args):
63        self._custom_exit()
64
65    def _custom_init(self):
66        pass
67
68    def _custom_exit(self):
69        pass
70
71    @property
72    def debugger_info(self):
73        return DebuggerIR(name=self.name, version=self.version)
74
75    @property
76    def is_available(self):
77        return self.has_loaded and self.loading_error is None
78
79    @property
80    def loading_error(self):
81        return str(self._loading_error[1]) if self._loading_error is not None else None
82
83    @property
84    def loading_error_trace(self):
85        if not self._loading_error:
86            return None
87
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_exception
93            )
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 tb
101
102    def _sanitize_function_name(self, name):  # pylint: disable=no-self-use
103        """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 name
108
109    @abc.abstractmethod
110    def _load_interface(self):
111        pass
112
113    @classmethod
114    def get_option_name(cls):
115        """Short name that will be used on the command line to specify this
116        debugger.
117        """
118        raise NotImplementedError()
119
120    @classmethod
121    def get_name(cls):
122        """Full name of this debugger."""
123        raise NotImplementedError()
124
125    @property
126    def name(self):
127        return self.__class__.get_name()
128
129    @property
130    def option_name(self):
131        return self.__class__.get_option_name()
132
133    @abc.abstractproperty
134    def version(self):
135        pass
136
137    @abc.abstractmethod
138    def clear_breakpoints(self):
139        pass
140
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 be
145        an int.
146        """
147        return self._add_breakpoint(self._external_to_debug_path(file_), line)
148
149    @abc.abstractmethod
150    def _add_breakpoint(self, file_, line):
151        """Returns a unique opaque breakpoint id."""
152        pass
153
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 be
158        an int.
159        """
160        return self._add_conditional_breakpoint(
161            self._external_to_debug_path(file_), line, condition
162        )
163
164    @abc.abstractmethod
165    def _add_conditional_breakpoint(self, file_, line, condition):
166        """Returns a unique opaque breakpoint id."""
167        pass
168
169    @abc.abstractmethod
170    def delete_breakpoints(self, ids):
171        """Delete a set of breakpoints by ids.
172
173        Raises a KeyError if, for any id, no breakpoint with that id exists.
174        """
175        pass
176
177    @abc.abstractmethod
178    def get_triggered_breakpoint_ids(self):
179        """Returns a set of opaque ids for just-triggered breakpoints."""
180        pass
181
182    @abc.abstractmethod
183    def launch(self):
184        pass
185
186    @abc.abstractmethod
187    def step(self):
188        pass
189
190    @abc.abstractmethod
191    def go(self) -> ReturnCode:
192        pass
193
194    def get_step_info(self, watches, step_index):
195        step_info = self._get_step_info(watches, step_index)
196        for frame in step_info.frames:
197            frame.loc.path = self._debug_to_external_path(frame.loc.path)
198        return step_info
199
200    @abc.abstractmethod
201    def _get_step_info(self, watches, step_index):
202        pass
203
204    @abc.abstractproperty
205    def is_running(self):
206        pass
207
208    @abc.abstractproperty
209    def is_finished(self):
210        pass
211
212    @abc.abstractproperty
213    def frames_below_main(self):
214        pass
215
216    @abc.abstractmethod
217    def evaluate_expression(self, expression, frame_idx=0) -> ValueIR:
218        pass
219
220    def _external_to_debug_path(self, path):
221        if not self.options.debugger_use_relative_paths:
222            return path
223        root_dir = self.options.source_root_dir
224        if not root_dir or not path:
225            return path
226        assert path.startswith(root_dir)
227        return path[len(root_dir) :].lstrip(os.path.sep)
228
229    def _debug_to_external_path(self, path):
230        if not self.options.debugger_use_relative_paths:
231            return path
232        if not path or not self.options.source_root_dir:
233            return path
234        for file in self.options.source_files:
235            if path.endswith(self._external_to_debug_path(file)):
236                return file
237        return path
238
239
240class TestDebuggerBase(unittest.TestCase):
241    class MockDebugger(DebuggerBase):
242        def __init__(self, context, *args):
243            super().__init__(context, *args)
244            self.step_info = None
245            self.breakpoint_file = None
246
247        def _add_breakpoint(self, file, line):
248            self.breakpoint_file = file
249
250        def _get_step_info(self, watches, step_index):
251            return self.step_info
252
253    def __init__(self, *args):
254        super().__init__(*args)
255        TestDebuggerBase.MockDebugger.__abstractmethods__ = set()
256        self.options = SimpleNamespace(source_root_dir="", source_files=[])
257        context = SimpleNamespace(options=self.options)
258        self.dbg = TestDebuggerBase.MockDebugger(context)
259
260    def _new_step(self, paths):
261        frames = [
262            FrameIR(
263                function=None,
264                is_inlined=False,
265                loc=LocIR(path=path, lineno=0, column=0),
266            )
267            for path in paths
268        ]
269        return StepIR(step_index=0, stop_reason=None, frames=frames)
270
271    def _step_paths(self, step):
272        return [frame.loc.path for frame in step.frames]
273
274    def test_add_breakpoint_no_source_root_dir(self):
275        self.options.debugger_use_relative_paths = True
276        self.options.source_root_dir = ""
277        path = os.path.join(os.path.sep + "root", "some_file")
278        self.dbg.add_breakpoint(path, 12)
279        self.assertEqual(path, self.dbg.breakpoint_file)
280
281    def test_add_breakpoint_with_source_root_dir(self):
282        self.options.debugger_use_relative_paths = True
283        self.options.source_root_dir = os.path.sep + "my_root"
284        path = os.path.join(self.options.source_root_dir, "some_file")
285        self.dbg.add_breakpoint(path, 12)
286        self.assertEqual("some_file", self.dbg.breakpoint_file)
287
288    def test_add_breakpoint_with_source_root_dir_slash_suffix(self):
289        self.options.debugger_use_relative_paths = True
290        self.options.source_root_dir = os.path.sep + "my_root" + os.path.sep
291        path = os.path.join(self.options.source_root_dir, "some_file")
292        self.dbg.add_breakpoint(path, 12)
293        self.assertEqual("some_file", self.dbg.breakpoint_file)
294
295    def test_get_step_info_no_source_root_dir(self):
296        self.options.debugger_use_relative_paths = True
297        path = os.path.join(os.path.sep + "root", "some_file")
298        self.dbg.step_info = self._new_step([path])
299        self.assertEqual([path], self._step_paths(self.dbg.get_step_info([], 0)))
300
301    def test_get_step_info_no_frames(self):
302        self.options.debugger_use_relative_paths = True
303        self.options.source_root_dir = os.path.sep + "my_root"
304        self.dbg.step_info = self._new_step([])
305        self.assertEqual([], self._step_paths(self.dbg.get_step_info([], 0)))
306
307    def test_get_step_info(self):
308        self.options.debugger_use_relative_paths = True
309        self.options.source_root_dir = os.path.sep + "my_root"
310        path = os.path.join(self.options.source_root_dir, "some_file")
311        self.options.source_files = [path]
312        other_path = os.path.join(os.path.sep + "other", "file")
313        dbg_path = os.path.join(os.path.sep + "dbg", "some_file")
314        self.dbg.step_info = self._new_step([None, other_path, dbg_path])
315        self.assertEqual(
316            [None, other_path, path], self._step_paths(self.dbg.get_step_info([], 0))
317        )
318