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