xref: /llvm-project/cross-project-tests/debuginfo-tests/dexter/dex/debugger/DebuggerBase.py (revision 7e46a721fc7ea46f72a4fcf81062a76d6539f61d)
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.Exceptions import NotYetLoadedDebuggerException
20from dex.utils.ReturnCode import ReturnCode
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
29                os.path.samefile(path, watch_path)):
30            return False
31    if watch_frame_idx != frame_idx:
32        return False
33    if watch_line_range and line_no not in list(watch_line_range):
34        return False
35    return True
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 = NotYetLoadedDebuggerException()
48        try:
49            self._interface = self._load_interface()
50            self.has_loaded = True
51            self._loading_error = None
52        except DebuggerException:
53            self._loading_error = sys.exc_info()
54
55    def __enter__(self):
56        try:
57            self._custom_init()
58            self.clear_breakpoints()
59        except DebuggerException:
60            self._loading_error = sys.exc_info()
61        return self
62
63    def __exit__(self, *args):
64        self._custom_exit()
65
66    def _custom_init(self):
67        pass
68
69    def _custom_exit(self):
70        pass
71
72    @property
73    def debugger_info(self):
74        return DebuggerIR(name=self.name, version=self.version)
75
76    @property
77    def is_available(self):
78        return self.has_loaded and self.loading_error is None
79
80    @property
81    def loading_error(self):
82        return (str(self._loading_error[1])
83                if self._loading_error is not None else None)
84
85    @property
86    def loading_error_trace(self):
87        if not self._loading_error:
88            return None
89
90        tb = traceback.format_exception(*self._loading_error)
91
92        if self._loading_error[1].orig_exception is not None:
93            orig_exception = traceback.format_exception(
94                *self._loading_error[1].orig_exception)
95
96            if ''.join(orig_exception) not in ''.join(tb):
97                tb.extend(['\n'])
98                tb.extend(orig_exception)
99
100        tb = ''.join(tb).splitlines(True)
101        return tb
102
103    def _sanitize_function_name(self, name):  # pylint: disable=no-self-use
104        """If the function name returned by the debugger needs any post-
105        processing to make it fit (for example, if it includes a byte offset),
106        do that here.
107        """
108        return name
109
110    @abc.abstractmethod
111    def _load_interface(self):
112        pass
113
114    @classmethod
115    def get_option_name(cls):
116        """Short name that will be used on the command line to specify this
117        debugger.
118        """
119        raise NotImplementedError()
120
121    @classmethod
122    def get_name(cls):
123        """Full name of this debugger."""
124        raise NotImplementedError()
125
126    @property
127    def name(self):
128        return self.__class__.get_name()
129
130    @property
131    def option_name(self):
132        return self.__class__.get_option_name()
133
134    @abc.abstractproperty
135    def version(self):
136        pass
137
138    @abc.abstractmethod
139    def clear_breakpoints(self):
140        pass
141
142    def add_breakpoint(self, file_, line):
143        """Returns a unique opaque breakpoint id.
144
145        The ID type depends on the debugger being used, but will probably be
146        an int.
147        """
148        return self._add_breakpoint(self._external_to_debug_path(file_), line)
149
150    @abc.abstractmethod
151    def _add_breakpoint(self, file_, line):
152        """Returns a unique opaque breakpoint id.
153        """
154        pass
155
156    def add_conditional_breakpoint(self, file_, line, condition):
157        """Returns a unique opaque breakpoint id.
158
159        The ID type depends on the debugger being used, but will probably be
160        an int.
161        """
162        return self._add_conditional_breakpoint(
163            self._external_to_debug_path(file_), line, condition)
164
165    @abc.abstractmethod
166    def _add_conditional_breakpoint(self, file_, line, condition):
167        """Returns a unique opaque breakpoint id.
168        """
169        pass
170
171    @abc.abstractmethod
172    def delete_breakpoint(self, id):
173        """Delete a breakpoint by id.
174
175        Raises a KeyError if no breakpoint with this id exists.
176        """
177        pass
178
179    @abc.abstractmethod
180    def get_triggered_breakpoint_ids(self):
181        """Returns a set of opaque ids for just-triggered breakpoints.
182        """
183        pass
184
185    @abc.abstractmethod
186    def launch(self):
187        pass
188
189    @abc.abstractmethod
190    def step(self):
191        pass
192
193    @abc.abstractmethod
194    def go(self) -> ReturnCode:
195        pass
196
197    def get_step_info(self, watches, step_index):
198        step_info = self._get_step_info(watches, step_index)
199        for frame in step_info.frames:
200            frame.loc.path = self._debug_to_external_path(frame.loc.path)
201        return step_info
202
203    @abc.abstractmethod
204    def _get_step_info(self, watches, step_index):
205        pass
206
207    @abc.abstractproperty
208    def is_running(self):
209        pass
210
211    @abc.abstractproperty
212    def is_finished(self):
213        pass
214
215    @abc.abstractproperty
216    def frames_below_main(self):
217        pass
218
219    @abc.abstractmethod
220    def evaluate_expression(self, expression, frame_idx=0) -> ValueIR:
221        pass
222
223    def _external_to_debug_path(self, path):
224        if not self.options.debugger_use_relative_paths:
225            return path
226        root_dir = self.options.source_root_dir
227        if not root_dir or not path:
228            return path
229        assert path.startswith(root_dir)
230        return path[len(root_dir):].lstrip(os.path.sep)
231
232    def _debug_to_external_path(self, path):
233        if not self.options.debugger_use_relative_paths:
234            return path
235        if not path or not self.options.source_root_dir:
236            return path
237        for file in self.options.source_files:
238            if path.endswith(self._external_to_debug_path(file)):
239                return file
240        return path
241
242class TestDebuggerBase(unittest.TestCase):
243
244    class MockDebugger(DebuggerBase):
245
246        def __init__(self, context, *args):
247            super().__init__(context, *args)
248            self.step_info = None
249            self.breakpoint_file = None
250
251        def _add_breakpoint(self, file, line):
252            self.breakpoint_file = file
253
254        def _get_step_info(self, watches, step_index):
255            return self.step_info
256
257    def __init__(self, *args):
258        super().__init__(*args)
259        TestDebuggerBase.MockDebugger.__abstractmethods__ = set()
260        self.options = SimpleNamespace(source_root_dir = '', source_files = [])
261        context = SimpleNamespace(options = self.options)
262        self.dbg = TestDebuggerBase.MockDebugger(context)
263
264    def _new_step(self, paths):
265        frames = [
266            FrameIR(
267                function=None,
268                is_inlined=False,
269                loc=LocIR(path=path, lineno=0, column=0)) for path in paths
270        ]
271        return StepIR(step_index=0, stop_reason=None, frames=frames)
272
273    def _step_paths(self, step):
274        return [frame.loc.path for frame in step.frames]
275
276    def test_add_breakpoint_no_source_root_dir(self):
277        self.options.debugger_use_relative_paths = True
278        self.options.source_root_dir = ''
279        self.dbg.add_breakpoint('/root/some_file', 12)
280        self.assertEqual('/root/some_file', self.dbg.breakpoint_file)
281
282    def test_add_breakpoint_with_source_root_dir(self):
283        self.options.debugger_use_relative_paths = True
284        self.options.source_root_dir = '/my_root'
285        self.dbg.add_breakpoint('/my_root/some_file', 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 = '/my_root/'
291        self.dbg.add_breakpoint('/my_root/some_file', 12)
292        self.assertEqual('some_file', self.dbg.breakpoint_file)
293
294    def test_get_step_info_no_source_root_dir(self):
295        self.options.debugger_use_relative_paths = True
296        self.dbg.step_info = self._new_step(['/root/some_file'])
297        self.assertEqual(['/root/some_file'],
298            self._step_paths(self.dbg.get_step_info([], 0)))
299
300    def test_get_step_info_no_frames(self):
301        self.options.debugger_use_relative_paths = True
302        self.options.source_root_dir = '/my_root'
303        self.dbg.step_info = self._new_step([])
304        self.assertEqual([],
305            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 = '/my_root'
310        self.options.source_files = ['/my_root/some_file']
311        self.dbg.step_info = self._new_step(
312            [None, '/other/file', '/dbg/some_file'])
313        self.assertEqual([None, '/other/file', '/my_root/some_file'],
314            self._step_paths(self.dbg.get_step_info([], 0)))
315