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