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