[Dexter] Add DexDeclareAddress command and address function
authorStephen Tozer <stephen.tozer@sony.com>
Wed, 1 Dec 2021 13:00:22 +0000 (13:00 +0000)
committerStephen Tozer <stephen.tozer@sony.com>
Wed, 1 Dec 2021 13:07:19 +0000 (13:07 +0000)
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

16 files changed:
cross-project-tests/debuginfo-tests/dexter/Commands.md
cross-project-tests/debuginfo-tests/dexter/dex/command/ParseCommand.py
cross-project-tests/debuginfo-tests/dexter/dex/command/commands/DexDeclareAddress.py [new file with mode: 0644]
cross-project-tests/debuginfo-tests/dexter/dex/command/commands/DexExpectWatchBase.py
cross-project-tests/debuginfo-tests/dexter/dex/heuristic/Heuristic.py
cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/penalty/missing_dex_address.cpp [new file with mode: 0644]
cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/dex_declare_address/address_after_ref.cpp [new file with mode: 0644]
cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/dex_declare_address/address_hit_count.cpp [new file with mode: 0644]
cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/dex_declare_address/expression_address.cpp [new file with mode: 0644]
cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/dex_declare_address/identical_address.cpp [new file with mode: 0644]
cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/dex_declare_address/multiple_address.cpp [new file with mode: 0644]
cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/dex_declare_address/offset_address.cpp [new file with mode: 0644]
cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/dex_declare_address/self_comparison.cpp [new file with mode: 0644]
cross-project-tests/debuginfo-tests/dexter/feature_tests/subtools/test/address_printing.cpp [new file with mode: 0644]
cross-project-tests/debuginfo-tests/dexter/feature_tests/subtools/test/err_duplicate_address.cpp [new file with mode: 0644]
cross-project-tests/debuginfo-tests/dexter/feature_tests/subtools/test/err_undeclared_addr.cpp [new file with mode: 0644]

index 4ea24b3..24f6f32 100644 (file)
@@ -9,6 +9,7 @@
 * [DexLimitSteps](Commands.md#DexLimitSteps)
 * [DexLabel](Commands.md#DexLabel)
 * [DexWatch](Commands.md#DexWatch)
+* [DexDeclareAddress](Commands.md#DexDeclareAddress)
 * [DexDeclareFile](Commands.md#DexDeclareFile)
 * [DexFinishTest](Commands.md#DexFinishTest)
 
@@ -234,6 +235,61 @@ arithmetic operators to get offsets from labels:
 This command does not contribute to the heuristic score.
 
 ----
+## DexDeclareAddress
+    DexDeclareAddress(declared_address, expr, **on_line[, **hit_count])
+
+    Args:
+        declared_address (str): The unique name of an address, which can be used
+                                in DexExpectWatch-commands.
+        expr (str): An expression to evaluate to provide the value of this
+                    address.
+        on_line (int): The line at which the value of the expression will be
+                       assigned to the address.
+        hit_count (int): If provided, reads the value of the source expression
+                         after the line has been stepped onto the given number
+                         of times ('hit_count = 0' gives default behaviour).
+
+### Description
+Declares a variable that can be used in DexExpectWatch- commands as an expected
+value by using the `address(str[, int])` function. This is primarily
+useful for checking the values of pointer variables, which are generally
+determined at run-time (and so cannot be consistently matched by a hard-coded
+expected value), but may be consistent relative to each other. An example use of
+this command is as follows, using a set of pointer variables "foo", "bar", and
+"baz":
+
+    DexDeclareAddress('my_addr', 'bar', on_line=12)
+    DexExpectWatchValue('foo', address('my_addr'), on_line=10)
+    DexExpectWatchValue('bar', address('my_addr'), on_line=12)
+    DexExpectWatchValue('baz', address('my_addr', 16), on_line=14)
+
+On the first line, we declare the name of our variable 'my_addr'. This name must
+be unique (the same name cannot be declared twice), and attempting to reference
+an undeclared variable with `address` will fail. The value of the address
+variable will be assigned as the value of 'bar' when line 12 is first stepped
+on.
+
+On lines 2-4, we use the `address` function to refer to our variable. The first
+usage occurs on line 10, before the line where 'my_addr' is assigned its value;
+this is a valid use, as we assign the address value and check for correctness
+after gathering all debug information for the test. Thus the first test command
+will pass if 'foo' on line 10 has the same value as 'bar' on line 12.
+
+The second command will pass iff 'bar' is available at line 12 - even if the
+variable and lines are identical in DexDeclareAddress and DexExpectWatchValue,
+the latter will still expect a valid value. Similarly, if the variable for a
+DexDeclareAddress command is not available at the given line, any test against
+that address will fail.
+
+The `address` function also accepts an optional integer argument representing an
+offset (which may be negative) to be applied to the address value, so
+`address('my_addr', 16)` resolves to `my_addr + 16`. In the above example, this
+means that we expect `baz == bar + 16`.
+
+### Heuristic
+This command does not contribute to the heuristic score.
+
+----
 ## DexDeclareFile
     DexDeclareFile(declared_file)
 
index 40cb36a..b0e7646 100644 (file)
@@ -19,11 +19,13 @@ from dex.utils.Exceptions import CommandParseError
 
 from dex.command.CommandBase import CommandBase
 from dex.command.commands.DexDeclareFile import DexDeclareFile
+from dex.command.commands.DexDeclareAddress import DexDeclareAddress
 from dex.command.commands.DexExpectProgramState import DexExpectProgramState
 from dex.command.commands.DexExpectStepKind import DexExpectStepKind
 from dex.command.commands.DexExpectStepOrder import DexExpectStepOrder
 from dex.command.commands.DexExpectWatchType import DexExpectWatchType
 from dex.command.commands.DexExpectWatchValue import DexExpectWatchValue
+from dex.command.commands.DexExpectWatchBase import AddressExpression, DexExpectWatchBase
 from dex.command.commands.DexLabel import DexLabel
 from dex.command.commands.DexLimitSteps import DexLimitSteps
 from dex.command.commands.DexFinishTest import DexFinishTest
@@ -39,6 +41,7 @@ def _get_valid_commands():
         { name (str): command (class) }
     """
     return {
+      DexDeclareAddress.get_name() : DexDeclareAddress,
       DexDeclareFile.get_name() : DexDeclareFile,
       DexExpectProgramState.get_name() : DexExpectProgramState,
       DexExpectStepKind.get_name() : DexExpectStepKind,
@@ -73,7 +76,7 @@ def _merge_subcommands(command_name: str, valid_commands: dict) -> dict:
     return valid_commands
 
 
-def _build_command(command_type, labels, raw_text: str, path: str, lineno: str) -> CommandBase:
+def _build_command(command_type, labels, addresses, raw_text: str, path: str, lineno: str) -> CommandBase:
     """Build a command object from raw text.
 
     This function will call eval().
@@ -90,9 +93,15 @@ def _build_command(command_type, labels, raw_text: str, path: str, lineno: str)
             return line
         raise format_unresolved_label_err(label_name, raw_text, path, lineno)
 
+    def get_address_object(address_name: str, offset: int=0):
+        if address_name not in addresses:
+            raise format_undeclared_address_err(address_name, raw_text, path, lineno)
+        return AddressExpression(address_name, offset)
+
     valid_commands = _merge_subcommands(
         command_type.get_name(), {
             'ref': label_to_line,
+            'address': get_address_object,
             command_type.get_name(): command_type,
         })
 
@@ -178,6 +187,14 @@ def format_unresolved_label_err(label: str, src: str, filename: str, lineno) ->
     err.info = f'Unresolved label: \'{label}\''
     return err
 
+def format_undeclared_address_err(address: str, src: str, filename: str, lineno) -> CommandParseError:
+    err = CommandParseError()
+    err.src = src
+    err.caret = '' # Don't bother trying to point to the bad address.
+    err.filename = filename
+    err.lineno = lineno
+    err.info = f'Undeclared address: \'{address}\''
+    return err
 
 def format_parse_err(msg: str, path: str, lines: list, point: TextPoint) -> CommandParseError:
     err = CommandParseError()
@@ -210,9 +227,25 @@ def add_line_label(labels, label, cmd_path, cmd_lineno):
         raise err
     labels[label.eval()] = label.get_line()
 
+def add_address(addresses, address, cmd_path, cmd_lineno):
+    # Enforce unique address variables.
+    address_name = address.get_address_name()
+    if address_name in addresses:
+        err = CommandParseError()
+        err.info = f'Found duplicate address: \'{address_name}\''
+        err.lineno = cmd_lineno
+        err.filename = cmd_path
+        err.src = address.raw_text
+        # Don't both trying to point to it since we're only printing the raw
+        # command, which isn't much text.
+        err.caret = ''
+        raise err
+    addresses.append(address_name)
 
 def _find_all_commands_in_file(path, file_lines, valid_commands, source_root_dir):
     labels = {} # dict of {name: line}.
+    addresses = [] # list of addresses.
+    address_resolutions = {}
     cmd_path = path
     declared_files = set()
     commands = defaultdict(dict)
@@ -258,6 +291,7 @@ def _find_all_commands_in_file(path, file_lines, valid_commands, source_root_dir
                 command = _build_command(
                     valid_commands[command_name],
                     labels,
+                    addresses,
                     raw_text,
                     cmd_path,
                     cmd_point.get_lineno(),
@@ -277,6 +311,8 @@ def _find_all_commands_in_file(path, file_lines, valid_commands, source_root_dir
             else:
                 if type(command) is DexLabel:
                     add_line_label(labels, command, path, cmd_point.get_lineno())
+                elif type(command) is DexDeclareAddress:
+                    add_address(addresses, command, path, cmd_point.get_lineno())
                 elif type(command) is DexDeclareFile:
                     cmd_path = command.declared_file
                     if not os.path.isabs(cmd_path):
diff --git a/cross-project-tests/debuginfo-tests/dexter/dex/command/commands/DexDeclareAddress.py b/cross-project-tests/debuginfo-tests/dexter/dex/command/commands/DexDeclareAddress.py
new file mode 100644 (file)
index 0000000..cf54760
--- /dev/null
@@ -0,0 +1,58 @@
+# 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
+"""Commmand sets the path for all following commands to 'declared_file'.
+"""
+
+import os
+
+from dex.command.CommandBase import CommandBase, StepExpectInfo
+
+class DexDeclareAddress(CommandBase):
+    def __init__(self, addr_name, expression, **kwargs):
+
+        if not isinstance(addr_name, str):
+            raise TypeError('invalid argument type')
+
+        self.addr_name = addr_name
+        self.expression = expression
+        self.on_line = kwargs.pop('on_line')
+        self.hit_count = kwargs.pop('hit_count', 0)
+
+        self.address_resolutions = None
+
+        super(DexDeclareAddress, self).__init__()
+
+    @staticmethod
+    def get_name():
+        return __class__.__name__
+
+    def get_watches(self):
+        return [StepExpectInfo(self.expression, self.path, 0, range(self.on_line, self.on_line + 1))]
+
+    def get_address_name(self):
+        return self.addr_name
+
+    def eval(self, step_collection):
+        assert os.path.exists(self.path)
+        self.address_resolutions[self.get_address_name()] = None
+        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 == self.on_line):
+                if self.hit_count > 0:
+                    self.hit_count -= 1
+                    continue
+                try:
+                    watch = step.program_state.frames[0].watches[self.expression]
+                except KeyError:
+                    pass
+                else:
+                    hex_val = int(watch.value, 16)
+                    self.address_resolutions[self.get_address_name()] = hex_val
+                    break
index 1c2d544..44c8bdb 100644 (file)
 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):
@@ -25,7 +46,7 @@ class DexExpectWatchBase(CommandBase):
             raise TypeError('expected at least two args')
 
         self.expression = args[0]
-        self.values = [str(arg) for arg in args[1:]]
+        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
@@ -66,8 +87,32 @@ class DexExpectWatchBase(CommandBase):
         # 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))]
@@ -78,11 +123,11 @@ class DexExpectWatchBase(CommandBase):
 
     @property
     def missing_values(self):
-        return sorted(list(self._missing_values))
+        return sorted(list(self.describe_value(v) for v in self._missing_values))
 
     @property
     def encountered_values(self):
-        return sorted(list(set(self.values) - self._missing_values))
+        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):
@@ -104,13 +149,25 @@ class DexExpectWatchBase(CommandBase):
             self.irretrievable_watches.append(step_info)
             return
 
-        if step_info.expected_value not in self.values:
+        # 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(step_info.expected_value)
+            self._missing_values.remove(value_to_remove)
         except KeyError:
             pass
 
@@ -177,8 +234,9 @@ class DexExpectWatchBase(CommandBase):
                     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 self.values if v in
+                    v for v in resolved_values if v in
                     [w.expected_value for w in self.expected_watches]
                 ])
index 205b767..0305c83 100644 (file)
@@ -15,6 +15,7 @@ import difflib
 import os
 from itertools import groupby
 from dex.command.StepValueInfo import StepValueInfo
+from dex.command.commands.DexExpectWatchBase import format_address
 
 
 PenaltyCommand = namedtuple('PenaltyCommand', ['pen_dict', 'max_penalty'])
@@ -101,6 +102,7 @@ class Heuristic(object):
     def __init__(self, context, steps):
         self.context = context
         self.penalties = {}
+        self.address_resolutions = {}
 
         worst_penalty = max([
             self.penalty_variable_optimized, self.penalty_irretrievable,
@@ -109,6 +111,14 @@ class Heuristic(object):
             self.penalty_missing_step, self.penalty_misordered_steps
         ])
 
+        # Before evaluating scoring commands, evaluate address values.
+        try:
+            for command in steps.commands['DexDeclareAddress']:
+                command.address_resolutions = self.address_resolutions
+                command.eval(steps)
+        except KeyError:
+            pass
+
         # Get DexExpectWatchType results.
         try:
             for command in steps.commands['DexExpectWatchType']:
@@ -126,6 +136,7 @@ class Heuristic(object):
         # Get DexExpectWatchValue results.
         try:
             for command in steps.commands['DexExpectWatchValue']:
+                command.address_resolutions = self.address_resolutions
                 command.eval(steps)
                 maximum_possible_penalty = min(3, len(
                     command.values)) * worst_penalty
@@ -425,6 +436,17 @@ class Heuristic(object):
     @property
     def verbose_output(self):  # noqa
         string = ''
+
+        # Add address resolutions if present.
+        if self.address_resolutions:
+            if self.resolved_addresses:
+                string += '\nResolved Addresses:\n'
+                for addr, res in self.resolved_addresses.items():
+                    string += f"  '{addr}': {res}\n"
+            if self.unresolved_addresses:
+                string += '\n'
+                string += f'Unresolved Addresses:\n  {self.unresolved_addresses}\n'
+
         string += ('\n')
         for command in sorted(self.penalties):
             pen_cmd = self.penalties[command]
@@ -457,6 +479,14 @@ class Heuristic(object):
         return string
 
     @property
+    def resolved_addresses(self):
+        return {addr: format_address(res) for addr, res in self.address_resolutions.items() if res is not None}
+
+    @property
+    def unresolved_addresses(self):
+        return [addr for addr, res in self.address_resolutions.items() if res is None]
+
+    @property
     def penalty_variable_optimized(self):
         return self.context.options.penalty_variable_optimized
 
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/penalty/missing_dex_address.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/penalty/missing_dex_address.cpp
new file mode 100644 (file)
index 0000000..0d3cc09
--- /dev/null
@@ -0,0 +1,20 @@
+// Purpose:
+//      Test that when a \DexDeclareAddress never resolves to a value, it is
+//      counted as a missing value in any \DexExpectWatchValues.
+//
+// REQUIRES: system-linux
+//
+// RUN: not %dexter_regression_test -- %s | FileCheck %s
+// CHECK: missing_dex_address.cpp
+
+int main() {
+    int *x = nullptr;
+    x = new int(5); // DexLabel('start_line')
+    if (false) {
+        (void)0; // DexLabel('unreachable')
+    }
+    delete x; // DexLabel('end_line')
+}
+
+// DexDeclareAddress('x', 'x', on_line=ref('unreachable'))
+// DexExpectWatchValue('x', 0, address('x'), from_line=ref('start_line'), to_line=ref('end_line'))
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/dex_declare_address/address_after_ref.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/dex_declare_address/address_after_ref.cpp
new file mode 100644 (file)
index 0000000..6ce9b8a
--- /dev/null
@@ -0,0 +1,17 @@
+// Purpose:
+//      Test that a \DexDeclareAddress value can have its value defined after
+//      the first reference to that value.
+//
+// REQUIRES: system-linux
+//
+// RUN: %dexter_regression_test -- %s | FileCheck %s
+// CHECK: address_after_ref.cpp
+
+int main() {
+    int *x = new int(5);
+    int *y = x; // DexLabel('first_line')
+    delete x; // DexLabel('last_line')
+}
+
+// DexDeclareAddress('y', 'y', on_line=ref('last_line'))
+// DexExpectWatchValue('x', address('y'), on_line=ref('first_line'))
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/dex_declare_address/address_hit_count.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/dex_declare_address/address_hit_count.cpp
new file mode 100644 (file)
index 0000000..c064ea9
--- /dev/null
@@ -0,0 +1,20 @@
+// Purpose:
+//      Test that a \DexDeclareAddress command can be passed 'hit_count' as an
+//      optional keyword argument that captures the value of the given
+//      expression after the target line has been stepped on a given number of
+//      times.
+//
+// REQUIRES: system-linux
+//
+// RUN: %dexter_regression_test -- %s | FileCheck %s
+// CHECK: address_hit_count.cpp
+
+int main() {
+    int *x = new int[3];
+    for (int *y = x; y < x + 3; ++y)
+      *y = 0; // DexLabel('test_line')
+    delete x;
+}
+
+// DexDeclareAddress('y', 'y', on_line=ref('test_line'), hit_count=2)
+// DexExpectWatchValue('y', address('y', -8), address('y', -4), address('y'), on_line=ref('test_line'))
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/dex_declare_address/expression_address.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/dex_declare_address/expression_address.cpp
new file mode 100644 (file)
index 0000000..72ad119
--- /dev/null
@@ -0,0 +1,18 @@
+// Purpose:
+//      Test that a \DexDeclareAddress value can be used to compare the
+//      addresses of two local variables that refer to the same address.
+//
+// REQUIRES: system-linux
+//
+// RUN: %dexter_regression_test -- %s | FileCheck %s
+// CHECK: expression_address.cpp
+
+int main() {
+    int x = 5;
+    int &y = x;
+    x = 3; // DexLabel('test_line')
+}
+
+// DexDeclareAddress('x_addr', '&x', on_line=ref('test_line'))
+// DexExpectWatchValue('&x', address('x_addr'), on_line=ref('test_line'))
+// DexExpectWatchValue('&y', address('x_addr'), on_line=ref('test_line'))
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/dex_declare_address/identical_address.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/dex_declare_address/identical_address.cpp
new file mode 100644 (file)
index 0000000..ea495d3
--- /dev/null
@@ -0,0 +1,18 @@
+// Purpose:
+//      Test that a \DexDeclareAddress value can be used to compare two equal
+//      pointer variables.
+//
+// REQUIRES: system-linux
+//
+// RUN: %dexter_regression_test -- %s | FileCheck %s
+// CHECK: identical_address.cpp
+
+int main() {
+    int *x = new int(5);
+    int *y = x;
+    delete x; // DexLabel('test_line')
+}
+
+// DexDeclareAddress('x', 'x', on_line=ref('test_line'))
+// DexExpectWatchValue('x', address('x'), on_line=ref('test_line'))
+// DexExpectWatchValue('y', address('x'), on_line=ref('test_line'))
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/dex_declare_address/multiple_address.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/dex_declare_address/multiple_address.cpp
new file mode 100644 (file)
index 0000000..c8bcbea
--- /dev/null
@@ -0,0 +1,24 @@
+// Purpose:
+//      Test that multiple \DexDeclareAddress references that point to different
+//      addresses can be used within a single \DexExpectWatchValue.
+//
+// REQUIRES: system-linux
+//
+// RUN: %dexter_regression_test -- %s | FileCheck %s
+// CHECK: multiple_address.cpp
+
+int main() {
+    int *x = new int(5);
+    int *y = new int(4);
+    int *z = x;
+    *z = 0; // DexLabel('start_line')
+    z = y;
+    *z = 0;
+    delete x; // DexLabel('end_line')
+    delete y;
+}
+
+// DexDeclareAddress('x', 'x', on_line=ref('start_line'))
+// DexDeclareAddress('y', 'y', on_line=ref('start_line'))
+// DexExpectWatchValue('z', address('x'), address('y'), from_line=ref('start_line'), to_line=ref('end_line'))
+// DexExpectWatchValue('*z', 5, 0, 4, 0, from_line=ref('start_line'), to_line=ref('end_line'))
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/dex_declare_address/offset_address.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/dex_declare_address/offset_address.cpp
new file mode 100644 (file)
index 0000000..e778cd1
--- /dev/null
@@ -0,0 +1,18 @@
+// Purpose:
+//      Test that a \DexDeclareAddress value can be used to compare two pointer
+//      variables that have a fixed offset between them.
+//
+// REQUIRES: system-linux
+//
+// RUN: %dexter_regression_test -- %s | FileCheck %s
+// CHECK: offset_address.cpp
+
+int main() {
+    int *x = new int[5];
+    int *y = x + 3;
+    delete x; // DexLabel('test_line')
+}
+
+// DexDeclareAddress('x', 'x', on_line=ref('test_line'))
+// DexExpectWatchValue('x', address('x'), on_line=ref('test_line'))
+// DexExpectWatchValue('y', address('x', 12), on_line=ref('test_line'))
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/dex_declare_address/self_comparison.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/commands/perfect/dex_declare_address/self_comparison.cpp
new file mode 100644 (file)
index 0000000..77801b7
--- /dev/null
@@ -0,0 +1,18 @@
+// Purpose:
+//      Test that a \DexDeclareAddress value can be used to check the change in
+//      value of a variable over time, relative to its initial value.
+//
+// REQUIRES: system-linux
+//
+// RUN: %dexter_regression_test -- %s | FileCheck %s
+// CHECK: self_comparison.cpp
+
+int main() {
+    int *x = new int[3];
+    for (int *y = x; y < x + 3; ++y)
+      *y = 0; // DexLabel('test_line')
+    delete x;
+}
+
+// DexDeclareAddress('y', 'y', on_line=ref('test_line'))
+// DexExpectWatchValue('y', address('y'), address('y', 4), address('y', 8), on_line=ref('test_line'))
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/subtools/test/address_printing.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/subtools/test/address_printing.cpp
new file mode 100644 (file)
index 0000000..0ee8a37
--- /dev/null
@@ -0,0 +1,51 @@
+// Purpose:
+//      Test that address values in a \DexExpectWatchValue are printed with
+//      their address name along with the address' resolved value (if any), and
+//      that when verbose output is enabled the complete map of resolved
+//      addresses and list of unresolved addresses will also be printed.
+//
+//      Note: Currently "misordered result" is the only penalty that does not
+//      display the address properly; if it is implemented, this test should be
+//      updated.
+//
+// REQUIRES: system-linux
+//
+// RUN: not %dexter_regression_test -v -- %s | FileCheck %s
+
+// CHECK: Resolved Addresses:
+// CHECK-NEXT: 'x_2': 0x[[X2_VAL:[0-9a-f]+]]
+// CHECK-NEXT: 'y': 0x[[Y_VAL:[0-9a-f]+]]
+// CHECK: Unresolved Addresses:
+// CHECK-NEXT: ['x_1']
+
+// CHECK-LABEL: [x] ExpectValue
+// CHECK: expected encountered watches:
+// CHECK-NEXT: address 'x_2' (0x[[X2_VAL]])
+// CHECK: missing values:
+// CHECK-NEXT: address 'x_1'
+
+// CHECK-LABEL: [z] ExpectValue
+// CHECK: expected encountered watches:
+// CHECK-NEXT: address 'x_2' (0x[[X2_VAL]])
+// CHECK-NEXT: address 'y' (0x[[Y_VAL]])
+// CHECK: misordered result:
+// CHECK-NEXT: step 4 (0x[[Y_VAL]])
+// CHECK-NEXT: step 5 (0x[[X2_VAL]])
+
+int main() {
+    int *x = new int(5);
+    int *y = new int(4);
+    if (false) {
+        (void)0; // DexLabel('unreachable')
+    }
+    int *z = y;
+    z = x; // DexLabel('start_line')
+    delete y;
+    delete x; // DexLabel('end_line')
+}
+
+// DexDeclareAddress('x_1', 'x', on_line=ref('unreachable'))
+// DexDeclareAddress('x_2', 'x', on_line=ref('end_line'))
+// DexDeclareAddress('y', 'y', on_line=ref('start_line'))
+// DexExpectWatchValue('x', address('x_1'), address('x_2'), from_line=ref('start_line'), to_line=ref('end_line'))
+// DexExpectWatchValue('z', address('x_2'), address('y'), from_line=ref('start_line'), to_line=ref('end_line'))
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/subtools/test/err_duplicate_address.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/subtools/test/err_duplicate_address.cpp
new file mode 100644 (file)
index 0000000..23bc1a5
--- /dev/null
@@ -0,0 +1,16 @@
+// Purpose:
+//      Check that declaring duplicate addresses gives a useful error message.
+//
+// RUN: not %dexter_regression_test -v -- %s | FileCheck %s --match-full-lines
+
+
+int main() {
+    int *result = new int(0);
+    delete result; // DexLabel('test_line')
+}
+
+// CHECK: parser error:{{.*}}err_duplicate_address.cpp([[# @LINE + 4]]): Found duplicate address: 'oops'
+// CHECK-NEXT: {{Dex}}DeclareAddress('oops', 'result', on_line=ref('test_line'))
+
+// DexDeclareAddress('oops', 'result', on_line=ref('test_line'))
+// DexDeclareAddress('oops', 'result', on_line=ref('test_line'))
diff --git a/cross-project-tests/debuginfo-tests/dexter/feature_tests/subtools/test/err_undeclared_addr.cpp b/cross-project-tests/debuginfo-tests/dexter/feature_tests/subtools/test/err_undeclared_addr.cpp
new file mode 100644 (file)
index 0000000..baae1ac
--- /dev/null
@@ -0,0 +1,16 @@
+// Purpose:
+//      Check that using an undeclared address gives a useful error message.
+//
+// RUN: not %dexter_regression_test -v -- %s | FileCheck %s --match-full-lines
+
+
+int main() {
+    int *result = new int(0);
+    delete result; // DexLabel('test_line')
+}
+
+
+// CHECK: parser error:{{.*}}err_undeclared_addr.cpp([[# @LINE + 3]]): Undeclared address: 'result'
+// CHECK-NEXT: {{Dex}}ExpectWatchValue('result', address('result'), on_line=ref('test_line'))
+
+// DexExpectWatchValue('result', address('result'), on_line=ref('test_line'))