xref: /llvm-project/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DebuggerBase.py (revision 75b316929a8fbdbe50f5fbd7e517d534c87795f0)
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
21def watch_is_active(watch_info: StepExpectInfo, path, frame_idx, line_no):
22    _, watch_path, watch_frame_idx, watch_line_range = watch_info
23    # If this watch should only be active for a specific file...
24    if watch_path and os.path.isfile(watch_path):
25        # If the current path does not match the expected file, this watch is
26        # not active.
27        if not (path and os.path.isfile(path) and
28                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
36class DebuggerBase(object, metaclass=abc.ABCMeta):
37    def __init__(self, context):
38        self.context = context
39        # Note: We can't already read values from options
40        # as DebuggerBase is created before we initialize options
41        # to read potential_debuggers.
42        self.options = self.context.options
43
44        self._interface = None
45        self.has_loaded = False
46        self._loading_error = None
47        try:
48            self._interface = self._load_interface()
49            self.has_loaded = True
50        except DebuggerException:
51            self._loading_error = sys.exc_info()
52
53    def __enter__(self):
54        try:
55            self._custom_init()
56            self.clear_breakpoints()
57        except DebuggerException:
58            self._loading_error = sys.exc_info()
59        return self
60
61    def __exit__(self, *args):
62        self._custom_exit()
63
64    def _custom_init(self):
65        pass
66
67    def _custom_exit(self):
68        pass
69
70    @property
71    def debugger_info(self):
72        return DebuggerIR(name=self.name, version=self.version)
73
74    @property
75    def is_available(self):
76        return self.has_loaded and self.loading_error is None
77
78    @property
79    def loading_error(self):
80        return (str(self._loading_error[1])
81                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            if ''.join(orig_exception) not in ''.join(tb):
95                tb.extend(['\n'])
96                tb.extend(orig_exception)
97
98        tb = ''.join(tb).splitlines(True)
99        return tb
100
101    def _sanitize_function_name(self, name):  # pylint: disable=no-self-use
102        """If the function name returned by the debugger needs any post-
103        processing to make it fit (for example, if it includes a byte offset),
104        do that here.
105        """
106        return name
107
108    @abc.abstractmethod
109    def _load_interface(self):
110        pass
111
112    @classmethod
113    def get_option_name(cls):
114        """Short name that will be used on the command line to specify this
115        debugger.
116        """
117        raise NotImplementedError()
118
119    @classmethod
120    def get_name(cls):
121        """Full name of this debugger."""
122        raise NotImplementedError()
123
124    @property
125    def name(self):
126        return self.__class__.get_name()
127
128    @property
129    def option_name(self):
130        return self.__class__.get_option_name()
131
132    @abc.abstractproperty
133    def version(self):
134        pass
135
136    @abc.abstractmethod
137    def clear_breakpoints(self):
138        pass
139
140    def add_breakpoint(self, file_, line):
141        """Returns a unique opaque breakpoint id.
142
143        The ID type depends on the debugger being used, but will probably be
144        an int.
145        """
146        return self._add_breakpoint(self._external_to_debug_path(file_), line)
147
148    @abc.abstractmethod
149    def _add_breakpoint(self, file_, line):
150        """Returns a unique opaque breakpoint id.
151        """
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    @abc.abstractmethod
164    def _add_conditional_breakpoint(self, file_, line, condition):
165        """Returns a unique opaque breakpoint id.
166        """
167        pass
168
169    @abc.abstractmethod
170    def delete_breakpoint(self, id):
171        """Delete a breakpoint by id.
172
173        Raises a KeyError if no breakpoint with this 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        """
181        pass
182
183    @abc.abstractmethod
184    def launch(self):
185        pass
186
187    @abc.abstractmethod
188    def step(self):
189        pass
190
191    @abc.abstractmethod
192    def go(self) -> ReturnCode:
193        pass
194
195    def get_step_info(self, watches, step_index):
196        step_info = self._get_step_info(watches, step_index)
197        for frame in step_info.frames:
198            frame.loc.path = self._debug_to_external_path(frame.loc.path)
199        return step_info
200
201    @abc.abstractmethod
202    def _get_step_info(self, watches, step_index):
203        pass
204
205    @abc.abstractproperty
206    def is_running(self):
207        pass
208
209    @abc.abstractproperty
210    def is_finished(self):
211        pass
212
213    @abc.abstractproperty
214    def frames_below_main(self):
215        pass
216
217    @abc.abstractmethod
218    def evaluate_expression(self, expression, frame_idx=0) -> ValueIR:
219        pass
220
221    def _external_to_debug_path(self, path):
222        if not self.options.debugger_use_relative_paths:
223            return path
224        root_dir = self.options.source_root_dir
225        if not root_dir or not path:
226            return path
227        assert path.startswith(root_dir)
228        return path[len(root_dir):].lstrip(os.path.sep)
229
230    def _debug_to_external_path(self, path):
231        if not self.options.debugger_use_relative_paths:
232            return path
233        if not path or not self.options.source_root_dir:
234            return path
235        for file in self.options.source_files:
236            if path.endswith(self._external_to_debug_path(file)):
237                return file
238        return path
239
240class TestDebuggerBase(unittest.TestCase):
241
242    class MockDebugger(DebuggerBase):
243
244        def __init__(self, context, *args):
245            super().__init__(context, *args)
246            self.step_info = None
247            self.breakpoint_file = None
248
249        def _add_breakpoint(self, file, line):
250            self.breakpoint_file = file
251
252        def _get_step_info(self, watches, step_index):
253            return self.step_info
254
255    def __init__(self, *args):
256        super().__init__(*args)
257        TestDebuggerBase.MockDebugger.__abstractmethods__ = set()
258        self.options = SimpleNamespace(source_root_dir = '', source_files = [])
259        context = SimpleNamespace(options = self.options)
260        self.dbg = TestDebuggerBase.MockDebugger(context)
261
262    def _new_step(self, paths):
263        frames = [
264            FrameIR(
265                function=None,
266                is_inlined=False,
267                loc=LocIR(path=path, lineno=0, column=0)) 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        self.dbg.add_breakpoint('/root/some_file', 12)
278        self.assertEqual('/root/some_file', self.dbg.breakpoint_file)
279
280    def test_add_breakpoint_with_source_root_dir(self):
281        self.options.debugger_use_relative_paths = True
282        self.options.source_root_dir = '/my_root'
283        self.dbg.add_breakpoint('/my_root/some_file', 12)
284        self.assertEqual('some_file', self.dbg.breakpoint_file)
285
286    def test_add_breakpoint_with_source_root_dir_slash_suffix(self):
287        self.options.debugger_use_relative_paths = True
288        self.options.source_root_dir = '/my_root/'
289        self.dbg.add_breakpoint('/my_root/some_file', 12)
290        self.assertEqual('some_file', self.dbg.breakpoint_file)
291
292    def test_get_step_info_no_source_root_dir(self):
293        self.options.debugger_use_relative_paths = True
294        self.dbg.step_info = self._new_step(['/root/some_file'])
295        self.assertEqual(['/root/some_file'],
296            self._step_paths(self.dbg.get_step_info([], 0)))
297
298    def test_get_step_info_no_frames(self):
299        self.options.debugger_use_relative_paths = True
300        self.options.source_root_dir = '/my_root'
301        self.dbg.step_info = self._new_step([])
302        self.assertEqual([],
303            self._step_paths(self.dbg.get_step_info([], 0)))
304
305    def test_get_step_info(self):
306        self.options.debugger_use_relative_paths = True
307        self.options.source_root_dir = '/my_root'
308        self.options.source_files = ['/my_root/some_file']
309        self.dbg.step_info = self._new_step(
310            [None, '/other/file', '/dbg/some_file'])
311        self.assertEqual([None, '/other/file', '/my_root/some_file'],
312            self._step_paths(self.dbg.get_step_info([], 0)))
313