xref: /llvm-project/cross-project-tests/debuginfo-tests/dexter/dex/command/commands/DexExpectWatchBase.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
8"""DexExpectWatch base class, holds logic for how to build and process expected
9 watch commands.
10"""
11
12import abc
13import difflib
14import os
15import math
16from collections import namedtuple
17from pathlib import PurePath
18
19from dex.command.CommandBase import CommandBase, StepExpectInfo
20from dex.command.StepValueInfo import StepValueInfo
21from dex.utils.Exceptions import NonFloatValueInCommand
22
23
24class AddressExpression(object):
25    def __init__(self, name, offset=0):
26        self.name = name
27        self.offset = offset
28
29    def is_resolved(self, resolutions):
30        return self.name in resolutions
31
32    # Given the resolved value of the address, resolve the final value of
33    # this expression.
34    def resolved_value(self, resolutions):
35        if not self.name in resolutions or resolutions[self.name] is None:
36            return None
37        # Technically we should fill(8) if we're debugging on a 32bit architecture?
38        return format_address(resolutions[self.name] + self.offset)
39
40
41def format_address(value, address_width=64):
42    return "0x" + hex(value)[2:].zfill(math.ceil(address_width / 4))
43
44
45def resolved_value(value, resolutions):
46    return (
47        value.resolved_value(resolutions)
48        if isinstance(value, AddressExpression)
49        else value
50    )
51
52
53class DexExpectWatchBase(CommandBase):
54    def __init__(self, *args, **kwargs):
55        if len(args) < 2:
56            raise TypeError("expected at least two args")
57
58        self.expression = args[0]
59        self.values = [
60            arg if isinstance(arg, AddressExpression) else str(arg) for arg in args[1:]
61        ]
62        try:
63            on_line = kwargs.pop("on_line")
64            self._from_line = on_line
65            self._to_line = on_line
66        except KeyError:
67            self._from_line = kwargs.pop("from_line", 1)
68            self._to_line = kwargs.pop("to_line", 999999)
69        self._require_in_order = kwargs.pop("require_in_order", True)
70        self.float_range = kwargs.pop("float_range", None)
71        if self.float_range is not None:
72            for value in self.values:
73                try:
74                    float(value)
75                except ValueError:
76                    raise NonFloatValueInCommand(
77                        f"Non-float value '{value}' when float_range arg provided"
78                    )
79        if kwargs:
80            raise TypeError("unexpected named args: {}".format(", ".join(kwargs)))
81
82        # Number of times that this watch has been encountered.
83        self.times_encountered = 0
84
85        # We'll pop from this set as we encounter values so anything left at
86        # the end can be considered as not having been seen.
87        self._missing_values = set(self.values)
88
89        self.misordered_watches = []
90
91        # List of StepValueInfos for any watch that is encountered as invalid.
92        self.invalid_watches = []
93
94        # List of StepValueInfo any any watch where we couldn't retrieve its
95        # data.
96        self.irretrievable_watches = []
97
98        # List of StepValueInfos for any watch that is encountered as having
99        # been optimized out.
100        self.optimized_out_watches = []
101
102        # List of StepValueInfos for any watch that is encountered that has an
103        # expected value.
104        self.expected_watches = []
105
106        # List of StepValueInfos for any watch that is encountered that has an
107        # unexpected value.
108        self.unexpected_watches = []
109
110        # List of StepValueInfos for all observed watches that were not
111        # invalid, irretrievable, or optimized out (combines expected and
112        # unexpected).
113        self.observed_watches = []
114
115        # dict of address names to their final resolved values, None until it
116        # gets assigned externally.
117        self.address_resolutions = None
118
119        super(DexExpectWatchBase, self).__init__()
120
121    def resolve_value(self, value):
122        return (
123            value.resolved_value(self.address_resolutions)
124            if isinstance(value, AddressExpression)
125            else value
126        )
127
128    def describe_value(self, value):
129        if isinstance(value, AddressExpression):
130            offset = ""
131            if value.offset > 0:
132                offset = f"+{value.offset}"
133            elif value.offset < 0:
134                offset = str(value.offset)
135            desc = f"address '{value.name}'{offset}"
136            if self.resolve_value(value) is not None:
137                desc += f" ({self.resolve_value(value)})"
138            return desc
139        return value
140
141    def get_watches(self):
142        return [
143            StepExpectInfo(
144                self.expression, self.path, 0, range(self._from_line, self._to_line + 1)
145            )
146        ]
147
148    @property
149    def line_range(self):
150        return list(range(self._from_line, self._to_line + 1))
151
152    @property
153    def missing_values(self):
154        return sorted(list(self.describe_value(v) for v in self._missing_values))
155
156    @property
157    def encountered_values(self):
158        return sorted(
159            list(
160                set(
161                    self.describe_value(v)
162                    for v in set(self.values) - self._missing_values
163                )
164            )
165        )
166
167    @abc.abstractmethod
168    def _get_expected_field(self, watch):
169        """Return a field from watch that this ExpectWatch command is checking."""
170
171    def _match_expected_floating_point(self, value):
172        """Checks to see whether value is a float that falls within the
173        acceptance range of one of this command's expected float values, and
174        returns the expected value if so; otherwise returns the original
175        value."""
176        try:
177            value_as_float = float(value)
178        except ValueError:
179            return value
180
181        possible_values = self.values
182        for expected in possible_values:
183            try:
184                expected_as_float = float(expected)
185                difference = abs(value_as_float - expected_as_float)
186                if difference <= self.float_range:
187                    return expected
188            except ValueError:
189                pass
190        return value
191
192    def _maybe_fix_float(self, value):
193        if self.float_range is not None:
194            return self._match_expected_floating_point(value)
195        else:
196            return value
197
198    def _handle_watch(self, step_info):
199        self.times_encountered += 1
200
201        if not step_info.watch_info.could_evaluate:
202            self.invalid_watches.append(step_info)
203            return
204
205        if step_info.watch_info.is_optimized_away:
206            self.optimized_out_watches.append(step_info)
207            return
208
209        if step_info.watch_info.is_irretrievable:
210            self.irretrievable_watches.append(step_info)
211            return
212
213        expected_value = self._maybe_fix_float(step_info.expected_value)
214
215        # Check to see if this value matches with a resolved address.
216        matching_address = None
217        for v in self.values:
218            if (
219                isinstance(v, AddressExpression)
220                and v.name in self.address_resolutions
221                and self.resolve_value(v) == expected_value
222            ):
223                matching_address = v
224                break
225
226        # If this is not an expected value, either a direct value or an address,
227        # then this is an unexpected watch.
228        if expected_value not in self.values and matching_address is None:
229            self.unexpected_watches.append(step_info)
230            return
231
232        self.expected_watches.append(step_info)
233        value_to_remove = (
234            matching_address if matching_address is not None else expected_value
235        )
236        try:
237            self._missing_values.remove(value_to_remove)
238        except KeyError:
239            pass
240
241    def _check_watch_order(self, actual_watches, expected_values):
242        """Use difflib to figure out whether the values are in the expected order
243        or not.
244        """
245        differences = []
246        actual_values = [
247            self._maybe_fix_float(w.expected_value) for w in actual_watches
248        ]
249        value_differences = list(
250            difflib.Differ().compare(actual_values, expected_values)
251        )
252
253        missing_value = False
254        index = 0
255        for vd in value_differences:
256            kind = vd[0]
257            if kind == "+":
258                # A value that is encountered in the expected list but not in the
259                # actual list.  We'll keep a note that something is wrong and flag
260                # the next value that matches as misordered.
261                missing_value = True
262            elif kind == " ":
263                # This value is as expected.  It might still be wrong if we've
264                # previously encountered a value that is in the expected list but
265                #  not the actual list.
266                if missing_value:
267                    missing_value = False
268                    differences.append(actual_watches[index])
269                index += 1
270            elif kind == "-":
271                # A value that is encountered in the actual list but not the
272                #  expected list.
273                differences.append(actual_watches[index])
274                index += 1
275            else:
276                assert False, "unexpected diff:{}".format(vd)
277
278        return differences
279
280    def eval(self, step_collection):
281        for step in step_collection.steps:
282            loc = step.current_location
283
284            if (
285                loc.path
286                and self.path
287                and PurePath(loc.path) == PurePath(self.path)
288                and loc.lineno in self.line_range
289            ):
290                try:
291                    watch = step.program_state.frames[0].watches[self.expression]
292                except KeyError:
293                    pass
294                else:
295                    expected_field = self._get_expected_field(watch)
296                    step_info = StepValueInfo(step.step_index, watch, expected_field)
297                    self._handle_watch(step_info)
298
299        if self._require_in_order:
300            # A list of all watches where the value has changed.
301            value_change_watches = []
302            prev_value = None
303            all_expected_values = []
304            for watch in self.expected_watches:
305                expected_value = self._maybe_fix_float(watch.expected_value)
306                all_expected_values.append(expected_value)
307                if expected_value != prev_value:
308                    value_change_watches.append(watch)
309                    prev_value = expected_value
310
311            resolved_values = [self.resolve_value(v) for v in self.values]
312            self.misordered_watches = self._check_watch_order(
313                value_change_watches,
314                [v for v in resolved_values if v in all_expected_values],
315            )
316