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