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