[Dexter] Allow Dexter watch commands to specify a range of acceptable FP values

This patch adds an optional argument to DexExpectWatchBase, float_range,
which defines a +- acceptance range for expected floating point values.
If passed, this assumes every expected value to be a floating point
value, and an exception will be thrown if this is not the case.

Differential Revision: https://reviews.llvm.org/D124511
This commit is contained in:
Stephen Tozer 2022-04-28 15:01:28 +01:00
parent 3f4a63e5f8
commit 30bb659c6f
10 changed files with 159 additions and 10 deletions

View File

@ -144,7 +144,7 @@ type checked against the list of `types`
---
## DexExpectWatchValue
DexExpectWatchValue(expr, *values [,**from_line=1][,**to_line=Max]
[,**on_line][,**require_in_order=True])
[,**on_line][,**require_in_order=True][,**float_range])
Args:
expr (str): expression to evaluate.
@ -159,6 +159,9 @@ type checked against the list of `types`
on_line (int): Only evaluate the expression on this line. If provided,
this overrides from_line and to_line.
require_in_order (bool): If False the values can appear in any order.
float_range (float): If provided, `values` must be floats, and will
match an actual value if they are within `float_range` of each other.
### Description
Expect the expression `expr` to evaluate to the list of `values`

View File

@ -15,7 +15,7 @@ from copy import copy
from pathlib import PurePath
from collections import defaultdict, OrderedDict
from dex.utils.Exceptions import CommandParseError
from dex.utils.Exceptions import CommandParseError, NonFloatValueInCommand
from dex.command.CommandBase import CommandBase
from dex.command.commands.DexCommandLine import DexCommandLine
@ -310,6 +310,10 @@ def _find_all_commands_in_file(path, file_lines, valid_commands, source_root_dir
err_point = copy(cmd_point)
err_point.char += len(command_name)
raise format_parse_err(str(e), path, file_lines, err_point)
except NonFloatValueInCommand as e:
err_point = copy(cmd_point)
err_point.char += len(command_name)
raise format_parse_err(str(e), path, file_lines, err_point)
else:
if type(command) is DexLabel:
add_line_label(labels, command, path, cmd_point.get_lineno())

View File

@ -18,6 +18,7 @@ from pathlib import PurePath
from dex.command.CommandBase import CommandBase, StepExpectInfo
from dex.command.StepValueInfo import StepValueInfo
from dex.utils.Exceptions import NonFloatValueInCommand
class AddressExpression(object):
def __init__(self, name, offset=0):
@ -56,6 +57,13 @@ class DexExpectWatchBase(CommandBase):
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)
self.float_range = kwargs.pop('float_range', None)
if self.float_range is not None:
for value in self.values:
try:
float(value)
except ValueError:
raise NonFloatValueInCommand(f'Non-float value \'{value}\' when float_range arg provided')
if kwargs:
raise TypeError('unexpected named args: {}'.format(
', '.join(kwargs)))
@ -135,6 +143,33 @@ class DexExpectWatchBase(CommandBase):
"""Return a field from watch that this ExpectWatch command is checking.
"""
def _match_expected_floating_point(self, value):
"""Checks to see whether value is a float that falls within the
acceptance range of one of this command's expected float values, and
returns the expected value if so; otherwise returns the original
value."""
try:
value_as_float = float(value)
except ValueError:
return value
possible_values = self.values
for expected in possible_values:
try:
expected_as_float = float(expected)
difference = abs(value_as_float - expected_as_float)
if difference <= self.float_range:
return expected
except ValueError:
pass
return value
def _maybe_fix_float(self, value):
if self.float_range is not None:
return self._match_expected_floating_point(value)
else:
return value
def _handle_watch(self, step_info):
self.times_encountered += 1
@ -150,23 +185,25 @@ class DexExpectWatchBase(CommandBase):
self.irretrievable_watches.append(step_info)
return
expected_value = self._maybe_fix_float(step_info.expected_value)
# 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):
self.resolve_value(v) == 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:
if 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
value_to_remove = matching_address if matching_address is not None else expected_value
try:
self._missing_values.remove(value_to_remove)
except KeyError:
@ -177,7 +214,7 @@ class DexExpectWatchBase(CommandBase):
or not.
"""
differences = []
actual_values = [w.expected_value for w in actual_watches]
actual_values = [self._maybe_fix_float(w.expected_value) for w in actual_watches]
value_differences = list(difflib.Differ().compare(actual_values,
expected_values))
@ -229,14 +266,16 @@ class DexExpectWatchBase(CommandBase):
# A list of all watches where the value has changed.
value_change_watches = []
prev_value = None
all_expected_values = []
for watch in self.expected_watches:
if watch.expected_value != prev_value:
expected_value = self._maybe_fix_float(watch.expected_value)
all_expected_values.append(expected_value)
if expected_value != prev_value:
value_change_watches.append(watch)
prev_value = watch.expected_value
prev_value = 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]
v for v in resolved_values if v in all_expected_values
])

View File

@ -54,6 +54,14 @@ class CommandParseError(Dexception):
self.caret = None
class NonFloatValueInCommand(CommandParseError):
"""If a command has the float_range arg but at least one of its expected
values cannot be converted to a float."""
def __init__(self, *args, **kwargs):
super(NonFloatValueInCommand, self).__init__(*args, **kwargs)
self.value = None
class ToolArgumentError(Dexception):
"""If a tool argument is invalid."""
pass

View File

@ -0,0 +1,16 @@
// Purpose:
// Check that a \DexExpectWatchValue float_range that is not large enough
// detects unexpected watch values.
//
// UNSUPPORTED: system-darwin
//
// RUN: not %dexter_regression_test -- %s | FileCheck %s
// CHECK: float_range_out_range.cpp:
int main() {
float a = 1.0f;
a = a - 0.5f;
return a; //DexLabel('check')
}
// DexExpectWatchValue('a', '1.00000', from_line=ref('check1'), to_line=ref('check2'), float_range=0.4)

View File

@ -0,0 +1,15 @@
// Purpose:
// Check that \DexExpectWatchValue float_range=0.0 matches only exact
// values.
//
// UNSUPPORTED: system-darwin
//
// RUN: not %dexter_regression_test -- %s | FileCheck %s
// CHECK: float_range_zero_nonmatch.cpp:
int main() {
float a = 1.0f;
return a; //DexLabel('check')
}
// DexExpectWatchValue('a', '1.0000001', on_line=ref('check'), float_range=0.0)

View File

@ -0,0 +1,18 @@
// Purpose:
// Check that \DexExpectWatchValue float_range=0.5 considers a range
// difference of 0.49999 to be an expected watch value for multple values.
//
// UNSUPPORTED: system-darwin
//
// RUN: %dexter_regression_test -- %s | FileCheck %s
// CHECK: float_range_multiple.cpp:
int main() {
float a = 1.0f;
float b = 100.f;
a = a + 0.4999f;
a = a + b; // DexLabel('check1')
return a; //DexLabel('check2')
}
// DexExpectWatchValue('a', '1.0', '100.0', from_line=ref('check1'), to_line=ref('check2'), float_range=0.5)

View File

@ -0,0 +1,16 @@
// Purpose:
// Check that omitted float_range from \DexExpectWatchValue turns off
// the floating point range evalution and defaults back to
// pre-float evalution.
//
// UNSUPPORTED: system-darwin
//
// RUN: %dexter_regression_test -- %s | FileCheck %s
// CHECK: float_range_no_arg.cpp:
int main() {
float a = 1.0f;
return a; //DexLabel('check')
}
// DexExpectWatchValue('a', '1.00000', on_line=ref('check'))

View File

@ -0,0 +1,16 @@
// Purpose:
// Check that \DexExpectWatchValue float_range=0.5 considers a range
// difference of 0.49999 to be an expected watch value.
//
// UNSUPPORTED: system-darwin
//
// RUN: %dexter_regression_test -- %s | FileCheck %s
// CHECK: float_range_small.cpp:
int main() {
float a = 1.0f;
a = a - 0.49999f;
return a; //DexLabel('check')
}
// DexExpectWatchValue('a', '1.0', on_line=ref('check'), float_range=0.5)

View File

@ -0,0 +1,14 @@
// Purpose:
// Check that \DexExpectWatchValue float_range=0.0 matches exact values.
//
// UNSUPPORTED: system-darwin
//
// RUN: %dexter_regression_test -- %s | FileCheck %s
// CHECK: float_range_zero_match.cpp:
int main() {
float a = 1.0f;
return a; //DexLabel('check')
}
// DexExpectWatchValue('a', '1.0000000', on_line=ref('check'), float_range=0.0)