This patch adds a new dexter command, DexDeclareAddress, which is used to test the relative values of pointer variables. The motivation for adding this command is to allow meaningful assertions to be made about pointers that go beyond checking variable availability and null equality. The full explanation and syntax is in Commands.md. Reviewed By: Orlando Differential Revision: https://reviews.llvm.org/D111447
243 lines
9.1 KiB
Python
243 lines
9.1 KiB
Python
# DExTer : Debugging Experience Tester
|
|
# ~~~~~~ ~ ~~ ~ ~~
|
|
#
|
|
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
# See https://llvm.org/LICENSE.txt for license information.
|
|
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
|
|
"""DexExpectWatch base class, holds logic for how to build and process expected
|
|
watch commands.
|
|
"""
|
|
|
|
import abc
|
|
import difflib
|
|
import os
|
|
import math
|
|
from collections import namedtuple
|
|
|
|
from dex.command.CommandBase import CommandBase, StepExpectInfo
|
|
from dex.command.StepValueInfo import StepValueInfo
|
|
|
|
class AddressExpression(object):
|
|
def __init__(self, name, offset=0):
|
|
self.name = name
|
|
self.offset = offset
|
|
|
|
def is_resolved(self, resolutions):
|
|
return self.name in resolutions
|
|
|
|
# Given the resolved value of the address, resolve the final value of
|
|
# this expression.
|
|
def resolved_value(self, resolutions):
|
|
if not self.name in resolutions or resolutions[self.name] is None:
|
|
return None
|
|
# Technically we should fill(8) if we're debugging on a 32bit architecture?
|
|
return format_address(resolutions[self.name] + self.offset)
|
|
|
|
def format_address(value, address_width=64):
|
|
return "0x" + hex(value)[2:].zfill(math.ceil(address_width/4))
|
|
|
|
def resolved_value(value, resolutions):
|
|
return value.resolved_value(resolutions) if isinstance(value, AddressExpression) else value
|
|
|
|
class DexExpectWatchBase(CommandBase):
|
|
def __init__(self, *args, **kwargs):
|
|
if len(args) < 2:
|
|
raise TypeError('expected at least two args')
|
|
|
|
self.expression = args[0]
|
|
self.values = [arg if isinstance(arg, AddressExpression) else str(arg) for arg in args[1:]]
|
|
try:
|
|
on_line = kwargs.pop('on_line')
|
|
self._from_line = on_line
|
|
self._to_line = on_line
|
|
except KeyError:
|
|
self._from_line = kwargs.pop('from_line', 1)
|
|
self._to_line = kwargs.pop('to_line', 999999)
|
|
self._require_in_order = kwargs.pop('require_in_order', True)
|
|
if kwargs:
|
|
raise TypeError('unexpected named args: {}'.format(
|
|
', '.join(kwargs)))
|
|
|
|
# Number of times that this watch has been encountered.
|
|
self.times_encountered = 0
|
|
|
|
# We'll pop from this set as we encounter values so anything left at
|
|
# the end can be considered as not having been seen.
|
|
self._missing_values = set(self.values)
|
|
|
|
self.misordered_watches = []
|
|
|
|
# List of StepValueInfos for any watch that is encountered as invalid.
|
|
self.invalid_watches = []
|
|
|
|
# List of StepValueInfo any any watch where we couldn't retrieve its
|
|
# data.
|
|
self.irretrievable_watches = []
|
|
|
|
# List of StepValueInfos for any watch that is encountered as having
|
|
# been optimized out.
|
|
self.optimized_out_watches = []
|
|
|
|
# List of StepValueInfos for any watch that is encountered that has an
|
|
# expected value.
|
|
self.expected_watches = []
|
|
|
|
# List of StepValueInfos for any watch that is encountered that has an
|
|
# unexpected value.
|
|
self.unexpected_watches = []
|
|
|
|
# List of StepValueInfos for all observed watches that were not
|
|
# invalid, irretrievable, or optimized out (combines expected and
|
|
# unexpected).
|
|
self.observed_watches = []
|
|
|
|
# dict of address names to their final resolved values, None until it
|
|
# gets assigned externally.
|
|
self.address_resolutions = None
|
|
|
|
super(DexExpectWatchBase, self).__init__()
|
|
|
|
def resolve_value(self, value):
|
|
return value.resolved_value(self.address_resolutions) if isinstance(value, AddressExpression) else value
|
|
|
|
def describe_value(self, value):
|
|
if isinstance(value, AddressExpression):
|
|
offset = ""
|
|
if value.offset > 0:
|
|
offset = f"+{value.offset}"
|
|
elif value.offset < 0:
|
|
offset = str(value.offset)
|
|
desc = f"address '{value.name}'{offset}"
|
|
if self.resolve_value(value) is not None:
|
|
desc += f" ({self.resolve_value(value)})"
|
|
return desc
|
|
return value
|
|
|
|
def get_watches(self):
|
|
return [StepExpectInfo(self.expression, self.path, 0, range(self._from_line, self._to_line + 1))]
|
|
|
|
@property
|
|
def line_range(self):
|
|
return list(range(self._from_line, self._to_line + 1))
|
|
|
|
@property
|
|
def missing_values(self):
|
|
return sorted(list(self.describe_value(v) for v in self._missing_values))
|
|
|
|
@property
|
|
def encountered_values(self):
|
|
return sorted(list(set(self.describe_value(v) for v in set(self.values) - self._missing_values)))
|
|
|
|
@abc.abstractmethod
|
|
def _get_expected_field(self, watch):
|
|
"""Return a field from watch that this ExpectWatch command is checking.
|
|
"""
|
|
|
|
def _handle_watch(self, step_info):
|
|
self.times_encountered += 1
|
|
|
|
if not step_info.watch_info.could_evaluate:
|
|
self.invalid_watches.append(step_info)
|
|
return
|
|
|
|
if step_info.watch_info.is_optimized_away:
|
|
self.optimized_out_watches.append(step_info)
|
|
return
|
|
|
|
if step_info.watch_info.is_irretrievable:
|
|
self.irretrievable_watches.append(step_info)
|
|
return
|
|
|
|
# Check to see if this value matches with a resolved address.
|
|
matching_address = None
|
|
for v in self.values:
|
|
if (isinstance(v, AddressExpression) and
|
|
v.name in self.address_resolutions and
|
|
self.resolve_value(v) == step_info.expected_value):
|
|
matching_address = v
|
|
break
|
|
|
|
# If this is not an expected value, either a direct value or an address,
|
|
# then this is an unexpected watch.
|
|
if step_info.expected_value not in self.values and matching_address is None:
|
|
self.unexpected_watches.append(step_info)
|
|
return
|
|
|
|
self.expected_watches.append(step_info)
|
|
value_to_remove = matching_address if matching_address is not None else step_info.expected_value
|
|
try:
|
|
self._missing_values.remove(value_to_remove)
|
|
except KeyError:
|
|
pass
|
|
|
|
def _check_watch_order(self, actual_watches, expected_values):
|
|
"""Use difflib to figure out whether the values are in the expected order
|
|
or not.
|
|
"""
|
|
differences = []
|
|
actual_values = [w.expected_value for w in actual_watches]
|
|
value_differences = list(difflib.Differ().compare(actual_values,
|
|
expected_values))
|
|
|
|
missing_value = False
|
|
index = 0
|
|
for vd in value_differences:
|
|
kind = vd[0]
|
|
if kind == '+':
|
|
# A value that is encountered in the expected list but not in the
|
|
# actual list. We'll keep a note that something is wrong and flag
|
|
# the next value that matches as misordered.
|
|
missing_value = True
|
|
elif kind == ' ':
|
|
# This value is as expected. It might still be wrong if we've
|
|
# previously encountered a value that is in the expected list but
|
|
# not the actual list.
|
|
if missing_value:
|
|
missing_value = False
|
|
differences.append(actual_watches[index])
|
|
index += 1
|
|
elif kind == '-':
|
|
# A value that is encountered in the actual list but not the
|
|
# expected list.
|
|
differences.append(actual_watches[index])
|
|
index += 1
|
|
else:
|
|
assert False, 'unexpected diff:{}'.format(vd)
|
|
|
|
return differences
|
|
|
|
def eval(self, step_collection):
|
|
assert os.path.exists(self.path)
|
|
for step in step_collection.steps:
|
|
loc = step.current_location
|
|
|
|
if (loc.path and os.path.exists(loc.path) and
|
|
os.path.samefile(loc.path, self.path) and
|
|
loc.lineno in self.line_range):
|
|
try:
|
|
watch = step.program_state.frames[0].watches[self.expression]
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
expected_field = self._get_expected_field(watch)
|
|
step_info = StepValueInfo(step.step_index, watch,
|
|
expected_field)
|
|
self._handle_watch(step_info)
|
|
|
|
if self._require_in_order:
|
|
# A list of all watches where the value has changed.
|
|
value_change_watches = []
|
|
prev_value = None
|
|
for watch in self.expected_watches:
|
|
if watch.expected_value != prev_value:
|
|
value_change_watches.append(watch)
|
|
prev_value = watch.expected_value
|
|
|
|
resolved_values = [self.resolve_value(v) for v in self.values]
|
|
self.misordered_watches = self._check_watch_order(
|
|
value_change_watches, [
|
|
v for v in resolved_values if v in
|
|
[w.expected_value for w in self.expected_watches]
|
|
])
|