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