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
15
16from dex.command.CommandBase import CommandBase
17from dex.command.StepValueInfo import StepValueInfo
18
19
20class DexExpectWatchBase(CommandBase):
21    def __init__(self, *args, **kwargs):
22        if len(args) < 2:
23            raise TypeError('expected at least two args')
24
25        self.expression = args[0]
26        self.values = [str(arg) for arg in args[1:]]
27        try:
28            on_line = kwargs.pop('on_line')
29            self._from_line = on_line
30            self._to_line = on_line
31        except KeyError:
32            self._from_line = kwargs.pop('from_line', 1)
33            self._to_line = kwargs.pop('to_line', 999999)
34        self._require_in_order = kwargs.pop('require_in_order', True)
35        if kwargs:
36            raise TypeError('unexpected named args: {}'.format(
37                ', '.join(kwargs)))
38
39        # Number of times that this watch has been encountered.
40        self.times_encountered = 0
41
42        # We'll pop from this set as we encounter values so anything left at
43        # the end can be considered as not having been seen.
44        self._missing_values = set(self.values)
45
46        self.misordered_watches = []
47
48        # List of StepValueInfos for any watch that is encountered as invalid.
49        self.invalid_watches = []
50
51        # List of StepValueInfo any any watch where we couldn't retrieve its
52        # data.
53        self.irretrievable_watches = []
54
55        # List of StepValueInfos for any watch that is encountered as having
56        # been optimized out.
57        self.optimized_out_watches = []
58
59        # List of StepValueInfos for any watch that is encountered that has an
60        # expected value.
61        self.expected_watches = []
62
63        # List of StepValueInfos for any watch that is encountered that has an
64        # unexpected value.
65        self.unexpected_watches = []
66
67        super(DexExpectWatchBase, self).__init__()
68
69
70    def get_watches(self):
71        return [self.expression]
72
73    @property
74    def line_range(self):
75        return list(range(self._from_line, self._to_line + 1))
76
77    @property
78    def missing_values(self):
79        return sorted(list(self._missing_values))
80
81    @property
82    def encountered_values(self):
83        return sorted(list(set(self.values) - self._missing_values))
84
85    @abc.abstractmethod
86    def _get_expected_field(self, watch):
87        """Return a field from watch that this ExpectWatch command is checking.
88        """
89
90    def _handle_watch(self, step_info):
91        self.times_encountered += 1
92
93        if not step_info.watch_info.could_evaluate:
94            self.invalid_watches.append(step_info)
95            return
96
97        if step_info.watch_info.is_optimized_away:
98            self.optimized_out_watches.append(step_info)
99            return
100
101        if step_info.watch_info.is_irretrievable:
102            self.irretrievable_watches.append(step_info)
103            return
104
105        if step_info.expected_value not in self.values:
106            self.unexpected_watches.append(step_info)
107            return
108
109        self.expected_watches.append(step_info)
110        try:
111            self._missing_values.remove(step_info.expected_value)
112        except KeyError:
113            pass
114
115    def _check_watch_order(self, actual_watches, expected_values):
116        """Use difflib to figure out whether the values are in the expected order
117        or not.
118        """
119        differences = []
120        actual_values = [w.expected_value for w in actual_watches]
121        value_differences = list(difflib.Differ().compare(actual_values,
122                                                          expected_values))
123
124        missing_value = False
125        index = 0
126        for vd in value_differences:
127            kind = vd[0]
128            if kind == '+':
129                # A value that is encountered in the expected list but not in the
130                # actual list.  We'll keep a note that something is wrong and flag
131                # the next value that matches as misordered.
132                missing_value = True
133            elif kind == ' ':
134                # This value is as expected.  It might still be wrong if we've
135                # previously encountered a value that is in the expected list but
136                #  not the actual list.
137                if missing_value:
138                    missing_value = False
139                    differences.append(actual_watches[index])
140                index += 1
141            elif kind == '-':
142                # A value that is encountered in the actual list but not the
143                #  expected list.
144                differences.append(actual_watches[index])
145                index += 1
146            else:
147                assert False, 'unexpected diff:{}'.format(vd)
148
149        return differences
150
151    def eval(self, step_collection):
152        for step in step_collection.steps:
153            loc = step.current_location
154
155            if (loc.path and os.path.exists(loc.path) and
156                os.path.exists(self.path) and
157                os.path.samefile(loc.path, self.path) and
158                loc.lineno in self.line_range):
159                try:
160                    watch = step.program_state.frames[0].watches[self.expression]
161                except KeyError:
162                    pass
163                else:
164                    expected_field = self._get_expected_field(watch)
165                    step_info = StepValueInfo(step.step_index, watch,
166                                              expected_field)
167                    self._handle_watch(step_info)
168
169        if self._require_in_order:
170            # A list of all watches where the value has changed.
171            value_change_watches = []
172            prev_value = None
173            for watch in self.expected_watches:
174                if watch.expected_value != prev_value:
175                    value_change_watches.append(watch)
176                    prev_value = watch.expected_value
177
178            self.misordered_watches = self._check_watch_order(
179                value_change_watches, [
180                    v for v in self.values if v in
181                    [w.expected_value for w in self.expected_watches]
182                ])
183