diff --git a/cross-project-tests/debuginfo-tests/dexter/Commands.md b/cross-project-tests/debuginfo-tests/dexter/Commands.md --- a/cross-project-tests/debuginfo-tests/dexter/Commands.md +++ b/cross-project-tests/debuginfo-tests/dexter/Commands.md @@ -144,7 +144,7 @@ --- ## 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 @@ 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` diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/command/ParseCommand.py b/cross-project-tests/debuginfo-tests/dexter/dex/command/ParseCommand.py --- a/cross-project-tests/debuginfo-tests/dexter/dex/command/ParseCommand.py +++ b/cross-project-tests/debuginfo-tests/dexter/dex/command/ParseCommand.py @@ -15,7 +15,7 @@ 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 @@ 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()) diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/command/commands/DexExpectWatchBase.py b/cross-project-tests/debuginfo-tests/dexter/dex/command/commands/DexExpectWatchBase.py --- a/cross-project-tests/debuginfo-tests/dexter/dex/command/commands/DexExpectWatchBase.py +++ b/cross-project-tests/debuginfo-tests/dexter/dex/command/commands/DexExpectWatchBase.py @@ -18,6 +18,7 @@ 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 @@ 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 @@ """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 @@ 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 @@ 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 @@ # 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 ]) diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/utils/Exceptions.py b/cross-project-tests/debuginfo-tests/dexter/dex/utils/Exceptions.py --- a/cross-project-tests/debuginfo-tests/dexter/dex/utils/Exceptions.py +++ b/cross-project-tests/debuginfo-tests/dexter/dex/utils/Exceptions.py @@ -54,6 +54,14 @@ 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 diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/penalty/float_range_out_range.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/penalty/float_range_out_range.cpp new file mode 100644 --- /dev/null +++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/penalty/float_range_out_range.cpp @@ -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) diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/penalty/float_range_zero_nonmatch.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/penalty/float_range_zero_nonmatch.cpp new file mode 100644 --- /dev/null +++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/penalty/float_range_zero_nonmatch.cpp @@ -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) diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/float_range_watch/float_range_multiple.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/float_range_watch/float_range_multiple.cpp new file mode 100644 --- /dev/null +++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/float_range_watch/float_range_multiple.cpp @@ -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) diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/float_range_watch/float_range_no_arg.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/float_range_watch/float_range_no_arg.cpp new file mode 100644 --- /dev/null +++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/float_range_watch/float_range_no_arg.cpp @@ -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')) diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/float_range_watch/float_range_small.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/float_range_watch/float_range_small.cpp new file mode 100644 --- /dev/null +++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/float_range_watch/float_range_small.cpp @@ -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) diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/float_range_watch/float_range_zero_match.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/float_range_watch/float_range_zero_match.cpp new file mode 100644 --- /dev/null +++ b/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/float_range_watch/float_range_zero_match.cpp @@ -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)