• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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        return self._add_breakpoint(self._external_to_debug_path(file_), line)
129
130    @abc.abstractmethod
131    def _add_breakpoint(self, file_, line):
132        pass
133
134    def add_conditional_breakpoint(self, file_, line, condition):
135        return self._add_conditional_breakpoint(
136            self._external_to_debug_path(file_), line, condition)
137
138    @abc.abstractmethod
139    def _add_conditional_breakpoint(self, file_, line, condition):
140        pass
141
142    def delete_conditional_breakpoint(self, file_, line, condition):
143        return self._delete_conditional_breakpoint(
144            self._external_to_debug_path(file_), line, condition)
145
146    @abc.abstractmethod
147    def _delete_conditional_breakpoint(self, file_, line, condition):
148        pass
149
150    @abc.abstractmethod
151    def launch(self):
152        pass
153
154    @abc.abstractmethod
155    def step(self):
156        pass
157
158    @abc.abstractmethod
159    def go(self) -> ReturnCode:
160        pass
161
162    def get_step_info(self, watches, step_index):
163        step_info = self._get_step_info(watches, step_index)
164        for frame in step_info.frames:
165            frame.loc.path = self._debug_to_external_path(frame.loc.path)
166        return step_info
167
168    @abc.abstractmethod
169    def _get_step_info(self, watches, step_index):
170        pass
171
172    @abc.abstractproperty
173    def is_running(self):
174        pass
175
176    @abc.abstractproperty
177    def is_finished(self):
178        pass
179
180    @abc.abstractproperty
181    def frames_below_main(self):
182        pass
183
184    @abc.abstractmethod
185    def evaluate_expression(self, expression, frame_idx=0) -> ValueIR:
186        pass
187
188    def _external_to_debug_path(self, path):
189        root_dir = self.options.source_root_dir
190        if not root_dir or not path:
191            return path
192        assert path.startswith(root_dir)
193        return path[len(root_dir):].lstrip(os.path.sep)
194
195    def _debug_to_external_path(self, path):
196        if not path or not self.options.source_root_dir:
197            return path
198        for file in self.options.source_files:
199            if path.endswith(self._external_to_debug_path(file)):
200                return file
201        return path
202
203class TestDebuggerBase(unittest.TestCase):
204
205    class MockDebugger(DebuggerBase):
206
207        def __init__(self, context, *args):
208            super().__init__(context, *args)
209            self.step_info = None
210            self.breakpoint_file = None
211
212        def _add_breakpoint(self, file, line):
213            self.breakpoint_file = file
214
215        def _get_step_info(self, watches, step_index):
216            return self.step_info
217
218    def __init__(self, *args):
219        super().__init__(*args)
220        TestDebuggerBase.MockDebugger.__abstractmethods__ = set()
221        self.options = SimpleNamespace(source_root_dir = '', source_files = [])
222        context = SimpleNamespace(options = self.options)
223        self.dbg = TestDebuggerBase.MockDebugger(context)
224
225    def _new_step(self, paths):
226        frames = [
227            FrameIR(
228                function=None,
229                is_inlined=False,
230                loc=LocIR(path=path, lineno=0, column=0)) for path in paths
231        ]
232        return StepIR(step_index=0, stop_reason=None, frames=frames)
233
234    def _step_paths(self, step):
235        return [frame.loc.path for frame in step.frames]
236
237    def test_add_breakpoint_no_source_root_dir(self):
238        self.options.source_root_dir = ''
239        self.dbg.add_breakpoint('/root/some_file', 12)
240        self.assertEqual('/root/some_file', self.dbg.breakpoint_file)
241
242    def test_add_breakpoint_with_source_root_dir(self):
243        self.options.source_root_dir = '/my_root'
244        self.dbg.add_breakpoint('/my_root/some_file', 12)
245        self.assertEqual('some_file', self.dbg.breakpoint_file)
246
247    def test_add_breakpoint_with_source_root_dir_slash_suffix(self):
248        self.options.source_root_dir = '/my_root/'
249        self.dbg.add_breakpoint('/my_root/some_file', 12)
250        self.assertEqual('some_file', self.dbg.breakpoint_file)
251
252    def test_get_step_info_no_source_root_dir(self):
253        self.dbg.step_info = self._new_step(['/root/some_file'])
254        self.assertEqual(['/root/some_file'],
255            self._step_paths(self.dbg.get_step_info([], 0)))
256
257    def test_get_step_info_no_frames(self):
258        self.options.source_root_dir = '/my_root'
259        self.dbg.step_info = self._new_step([])
260        self.assertEqual([],
261            self._step_paths(self.dbg.get_step_info([], 0)))
262
263    def test_get_step_info(self):
264        self.options.source_root_dir = '/my_root'
265        self.options.source_files = ['/my_root/some_file']
266        self.dbg.step_info = self._new_step(
267            [None, '/other/file', '/dbg/some_file'])
268        self.assertEqual([None, '/other/file', '/my_root/some_file'],
269            self._step_paths(self.dbg.get_step_info([], 0)))
270