summaryrefslogtreecommitdiffstats
path: root/debuginfo-tests/dexter/dex/command
diff options
context:
space:
mode:
Diffstat (limited to 'debuginfo-tests/dexter/dex/command')
-rw-r--r--debuginfo-tests/dexter/dex/command/CommandBase.py54
-rw-r--r--debuginfo-tests/dexter/dex/command/ParseCommand.py421
-rw-r--r--debuginfo-tests/dexter/dex/command/StepValueInfo.py23
-rw-r--r--debuginfo-tests/dexter/dex/command/__init__.py9
-rw-r--r--debuginfo-tests/dexter/dex/command/commands/DexExpectProgramState.py83
-rw-r--r--debuginfo-tests/dexter/dex/command/commands/DexExpectStepKind.py45
-rw-r--r--debuginfo-tests/dexter/dex/command/commands/DexExpectStepOrder.py39
-rw-r--r--debuginfo-tests/dexter/dex/command/commands/DexExpectWatchBase.py197
-rw-r--r--debuginfo-tests/dexter/dex/command/commands/DexExpectWatchType.py26
-rw-r--r--debuginfo-tests/dexter/dex/command/commands/DexExpectWatchValue.py27
-rw-r--r--debuginfo-tests/dexter/dex/command/commands/DexLabel.py31
-rw-r--r--debuginfo-tests/dexter/dex/command/commands/DexUnreachable.py38
-rw-r--r--debuginfo-tests/dexter/dex/command/commands/DexWatch.py39
13 files changed, 1032 insertions, 0 deletions
diff --git a/debuginfo-tests/dexter/dex/command/CommandBase.py b/debuginfo-tests/dexter/dex/command/CommandBase.py
new file mode 100644
index 00000000000..49e908623df
--- /dev/null
+++ b/debuginfo-tests/dexter/dex/command/CommandBase.py
@@ -0,0 +1,54 @@
+# 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
+"""Base class for all DExTer commands, where a command is a specific Python
+function that can be embedded into a comment in the source code under test
+which will then be executed by DExTer during debugging.
+"""
+
+import abc
+from typing import List
+
+class CommandBase(object, metaclass=abc.ABCMeta):
+ def __init__(self):
+ self.path = None
+ self.lineno = None
+ self.raw_text = ''
+
+ def get_label_args(self):
+ return list()
+
+ def has_labels(self):
+ return False
+
+ @abc.abstractstaticmethod
+ def get_name():
+ """This abstract method is usually implemented in subclasses as:
+ return __class__.__name__
+ """
+
+ def get_watches(self) -> List[str]:
+ return []
+
+ @abc.abstractmethod
+ def eval(self):
+ """Evaluate the command.
+
+ This will be called when constructing a Heuristic object to determine
+ the debug score.
+
+ Returns:
+ The logic for handling the result of CommandBase.eval() must be
+ defined in Heuristic.__init__() so a consitent return type between
+ commands is not enforced.
+ """
+
+ @staticmethod
+ def get_subcommands() -> dict:
+ """Returns a dictionary of subcommands in the form {name: command} or
+ None if no subcommands are required.
+ """
+ return None
diff --git a/debuginfo-tests/dexter/dex/command/ParseCommand.py b/debuginfo-tests/dexter/dex/command/ParseCommand.py
new file mode 100644
index 00000000000..3b9a2d5766b
--- /dev/null
+++ b/debuginfo-tests/dexter/dex/command/ParseCommand.py
@@ -0,0 +1,421 @@
+# 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
+"""Parse a DExTer command. In particular, ensure that only a very limited
+subset of Python is allowed, in order to prevent the possibility of unsafe
+Python code being embedded within DExTer commands.
+"""
+
+import os
+import unittest
+from copy import copy
+
+from collections import defaultdict
+
+from dex.utils.Exceptions import CommandParseError
+
+from dex.command.CommandBase import CommandBase
+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.DexLabel import DexLabel
+from dex.command.commands.DexUnreachable import DexUnreachable
+from dex.command.commands.DexWatch import DexWatch
+
+
+def _get_valid_commands():
+ """Return all top level DExTer test commands.
+
+ Returns:
+ { name (str): command (class) }
+ """
+ return {
+ DexExpectProgramState.get_name() : DexExpectProgramState,
+ DexExpectStepKind.get_name() : DexExpectStepKind,
+ DexExpectStepOrder.get_name() : DexExpectStepOrder,
+ DexExpectWatchType.get_name() : DexExpectWatchType,
+ DexExpectWatchValue.get_name() : DexExpectWatchValue,
+ DexLabel.get_name() : DexLabel,
+ DexUnreachable.get_name() : DexUnreachable,
+ DexWatch.get_name() : DexWatch
+ }
+
+
+def _get_command_name(command_raw: str) -> str:
+ """Return command name by splitting up DExTer command contained in
+ command_raw on the first opening paranthesis and further stripping
+ any potential leading or trailing whitespace.
+ """
+ return command_raw.split('(', 1)[0].rstrip()
+
+
+def _merge_subcommands(command_name: str, valid_commands: dict) -> dict:
+ """Merge valid_commands and command_name's subcommands into a new dict.
+
+ Returns:
+ { name (str): command (class) }
+ """
+ subcommands = valid_commands[command_name].get_subcommands()
+ if subcommands:
+ return { **valid_commands, **subcommands }
+ return valid_commands
+
+
+def _build_command(command_type, raw_text: str, path: str, lineno: str) -> CommandBase:
+ """Build a command object from raw text.
+
+ This function will call eval().
+
+ Raises:
+ Any exception that eval() can raise.
+
+ Returns:
+ A dexter command object.
+ """
+ valid_commands = _merge_subcommands(
+ command_type.get_name(), { command_type.get_name(): command_type })
+ # pylint: disable=eval-used
+ command = eval(raw_text, valid_commands)
+ # pylint: enable=eval-used
+ command.raw_text = raw_text
+ command.path = path
+ command.lineno = lineno
+ return command
+
+
+def resolve_labels(command: CommandBase, commands: dict):
+ """Attempt to resolve any labels in command"""
+ dex_labels = commands['DexLabel']
+ command_label_args = command.get_label_args()
+ for command_arg in command_label_args:
+ for dex_label in list(dex_labels.values()):
+ if (os.path.samefile(dex_label.path, command.path) and
+ dex_label.eval() == command_arg):
+ command.resolve_label(dex_label.get_as_pair())
+ # labels for command should be resolved by this point.
+ if command.has_labels():
+ syntax_error = SyntaxError()
+ syntax_error.filename = command.path
+ syntax_error.lineno = command.lineno
+ syntax_error.offset = 0
+ syntax_error.msg = 'Unresolved labels'
+ for label in command.get_label_args():
+ syntax_error.msg += ' \'' + label + '\''
+ raise syntax_error
+
+
+def _search_line_for_cmd_start(line: str, start: int, valid_commands: dict) -> int:
+ """Scan `line` for a string matching any key in `valid_commands`.
+
+ Start searching from `start`.
+ Commands escaped with `\` (E.g. `\DexLabel('a')`) are ignored.
+
+ Returns:
+ int: the index of the first character of the matching string in `line`
+ or -1 if no command is found.
+ """
+ for command in valid_commands:
+ idx = line.find(command, start)
+ if idx != -1:
+ # Ignore escaped '\' commands.
+ if idx > 0 and line[idx - 1] == '\\':
+ continue
+ return idx
+ return -1
+
+
+def _search_line_for_cmd_end(line: str, start: int, paren_balance: int) -> (int, int):
+ """Find the end of a command by looking for balanced parentheses.
+
+ Args:
+ line: String to scan.
+ start: Index into `line` to start looking.
+ paren_balance(int): paren_balance after previous call.
+
+ Note:
+ On the first call `start` should point at the opening parenthesis and
+ `paren_balance` should be set to 0. Subsequent calls should pass in the
+ returned `paren_balance`.
+
+ Returns:
+ ( end, paren_balance )
+ Where end is 1 + the index of the last char in the command or, if the
+ parentheses are not balanced, the end of the line.
+
+ paren_balance will be 0 when the parentheses are balanced.
+ """
+ for end in range(start, len(line)):
+ ch = line[end]
+ if ch == '(':
+ paren_balance += 1
+ elif ch == ')':
+ paren_balance -=1
+ if paren_balance == 0:
+ break
+ end += 1
+ return (end, paren_balance)
+
+
+class TextPoint():
+ def __init__(self, line, char):
+ self.line = line
+ self.char = char
+
+ def get_lineno(self):
+ return self.line + 1
+
+ def get_column(self):
+ return self.char + 1
+
+
+def format_parse_err(msg: str, path: str, lines: list, point: TextPoint) -> CommandParseError:
+ err = CommandParseError()
+ err.filename = path
+ err.src = lines[point.line].rstrip()
+ err.lineno = point.get_lineno()
+ err.info = msg
+ err.caret = '{}<r>^</>'.format(' ' * (point.char))
+ return err
+
+
+def skip_horizontal_whitespace(line, point):
+ for idx, char in enumerate(line[point.char:]):
+ if char not in ' \t':
+ point.char += idx
+ return
+
+
+def _find_all_commands_in_file(path, file_lines, valid_commands):
+ commands = defaultdict(dict)
+ paren_balance = 0
+ region_start = TextPoint(0, 0)
+ for region_start.line in range(len(file_lines)):
+ line = file_lines[region_start.line]
+ region_start.char = 0
+
+ # Search this line till we find no more commands.
+ while True:
+ # If parens are currently balanced we can look for a new command.
+ if paren_balance == 0:
+ region_start.char = _search_line_for_cmd_start(line, region_start.char, valid_commands)
+ if region_start.char == -1:
+ break # Read next line.
+
+ command_name = _get_command_name(line[region_start.char:])
+ cmd_point = copy(region_start)
+ cmd_text_list = [command_name]
+
+ region_start.char += len(command_name) # Start searching for parens after cmd.
+ skip_horizontal_whitespace(line, region_start)
+ if region_start.char >= len(line) or line[region_start.char] != '(':
+ raise format_parse_err(
+ "Missing open parenthesis", path, file_lines, region_start)
+
+ end, paren_balance = _search_line_for_cmd_end(line, region_start.char, paren_balance)
+ # Add this text blob to the command.
+ cmd_text_list.append(line[region_start.char:end])
+ # Move parse ptr to end of line or parens
+ region_start.char = end
+
+ # If the parens are unbalanced start reading the next line in an attempt
+ # to find the end of the command.
+ if paren_balance != 0:
+ break # Read next line.
+
+ # Parens are balanced, we have a full command to evaluate.
+ raw_text = "".join(cmd_text_list)
+ try:
+ command = _build_command(
+ valid_commands[command_name],
+ raw_text,
+ path,
+ cmd_point.get_lineno(),
+ )
+ except SyntaxError as e:
+ # This err should point to the problem line.
+ err_point = copy(cmd_point)
+ # To e the command start is the absolute start, so use as offset.
+ err_point.line += e.lineno - 1 # e.lineno is a position, not index.
+ err_point.char += e.offset - 1 # e.offset is a position, not index.
+ raise format_parse_err(e.msg, path, file_lines, err_point)
+ except TypeError as e:
+ # This err should always point to the end of the command name.
+ err_point = copy(cmd_point)
+ err_point.char += len(command_name)
+ raise format_parse_err(str(e), path, file_lines, err_point)
+ else:
+ resolve_labels(command, commands)
+ assert (path, cmd_point) not in commands[command_name], (
+ command_name, commands[command_name])
+ commands[command_name][path, cmd_point] = command
+
+ if paren_balance != 0:
+ # This err should always point to the end of the command name.
+ err_point = copy(cmd_point)
+ err_point.char += len(command_name)
+ msg = "Unbalanced parenthesis starting here"
+ raise format_parse_err(msg, path, file_lines, err_point)
+ return dict(commands)
+
+
+
+def find_all_commands(source_files):
+ commands = defaultdict(dict)
+ valid_commands = _get_valid_commands()
+ for source_file in source_files:
+ with open(source_file) as fp:
+ lines = fp.readlines()
+ file_commands = _find_all_commands_in_file(source_file, lines,
+ valid_commands)
+ for command_name in file_commands:
+ commands[command_name].update(file_commands[command_name])
+
+ return dict(commands)
+
+
+class TestParseCommand(unittest.TestCase):
+ class MockCmd(CommandBase):
+ """A mock DExTer command for testing parsing.
+
+ Args:
+ value (str): Unique name for this instance.
+ """
+
+ def __init__(self, *args):
+ self.value = args[0]
+
+ def get_name():
+ return __class__.__name__
+
+ def eval(this):
+ pass
+
+
+ def __init__(self, *args):
+ super().__init__(*args)
+
+ self.valid_commands = {
+ TestParseCommand.MockCmd.get_name() : TestParseCommand.MockCmd
+ }
+
+
+ def _find_all_commands_in_lines(self, lines):
+ """Use DExTer parsing methods to find all the mock commands in lines.
+
+ Returns:
+ { cmd_name: { (path, line): command_obj } }
+ """
+ return _find_all_commands_in_file(__file__, lines, self.valid_commands)
+
+
+ def _find_all_mock_values_in_lines(self, lines):
+ """Use DExTer parsing methods to find all mock command values in lines.
+
+ Returns:
+ values (list(str)): MockCmd values found in lines.
+ """
+ cmds = self._find_all_commands_in_lines(lines)
+ mocks = cmds.get(TestParseCommand.MockCmd.get_name(), None)
+ return [v.value for v in mocks.values()] if mocks else []
+
+
+ def test_parse_inline(self):
+ """Commands can be embedded in other text."""
+
+ lines = [
+ 'MockCmd("START") Lorem ipsum dolor sit amet, consectetur\n',
+ 'adipiscing elit, MockCmd("EMBEDDED") sed doeiusmod tempor,\n',
+ 'incididunt ut labore et dolore magna aliqua.\n'
+ ]
+
+ values = self._find_all_mock_values_in_lines(lines)
+
+ self.assertTrue('START' in values)
+ self.assertTrue('EMBEDDED' in values)
+
+
+ def test_parse_multi_line_comment(self):
+ """Multi-line commands can embed comments."""
+
+ lines = [
+ 'Lorem ipsum dolor sit amet, consectetur\n',
+ 'adipiscing elit, sed doeiusmod tempor,\n',
+ 'incididunt ut labore et MockCmd(\n',
+ ' "WITH_COMMENT" # THIS IS A COMMENT\n',
+ ') dolore magna aliqua. Ut enim ad minim\n',
+ ]
+
+ values = self._find_all_mock_values_in_lines(lines)
+
+ self.assertTrue('WITH_COMMENT' in values)
+
+ def test_parse_empty(self):
+ """Empty files are silently ignored."""
+
+ lines = []
+ values = self._find_all_mock_values_in_lines(lines)
+ self.assertTrue(len(values) == 0)
+
+ def test_parse_bad_whitespace(self):
+ """Throw exception when parsing badly formed whitespace."""
+ lines = [
+ 'MockCmd\n',
+ '("XFAIL_CMD_LF_PAREN")\n',
+ ]
+
+ with self.assertRaises(CommandParseError):
+ values = self._find_all_mock_values_in_lines(lines)
+
+ def test_parse_good_whitespace(self):
+ """Try to emulate python whitespace rules"""
+
+ lines = [
+ 'MockCmd("NONE")\n',
+ 'MockCmd ("SPACE")\n',
+ 'MockCmd\t\t("TABS")\n',
+ 'MockCmd( "ARG_SPACE" )\n',
+ 'MockCmd(\t\t"ARG_TABS"\t\t)\n',
+ 'MockCmd(\n',
+ '"CMD_PAREN_LF")\n',
+ ]
+
+ values = self._find_all_mock_values_in_lines(lines)
+
+ self.assertTrue('NONE' in values)
+ self.assertTrue('SPACE' in values)
+ self.assertTrue('TABS' in values)
+ self.assertTrue('ARG_SPACE' in values)
+ self.assertTrue('ARG_TABS' in values)
+ self.assertTrue('CMD_PAREN_LF' in values)
+
+
+ def test_parse_share_line(self):
+ """More than one command can appear on one line."""
+
+ lines = [
+ 'MockCmd("START") MockCmd("CONSECUTIVE") words '
+ 'MockCmd("EMBEDDED") more words\n'
+ ]
+
+ values = self._find_all_mock_values_in_lines(lines)
+
+ self.assertTrue('START' in values)
+ self.assertTrue('CONSECUTIVE' in values)
+ self.assertTrue('EMBEDDED' in values)
+
+
+ def test_parse_escaped(self):
+ """Escaped commands are ignored."""
+
+ lines = [
+ 'words \MockCmd("IGNORED") words words words\n'
+ ]
+
+ values = self._find_all_mock_values_in_lines(lines)
+
+ self.assertFalse('IGNORED' in values)
diff --git a/debuginfo-tests/dexter/dex/command/StepValueInfo.py b/debuginfo-tests/dexter/dex/command/StepValueInfo.py
new file mode 100644
index 00000000000..afcb9c5d0c8
--- /dev/null
+++ b/debuginfo-tests/dexter/dex/command/StepValueInfo.py
@@ -0,0 +1,23 @@
+# 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
+
+
+class StepValueInfo(object):
+ def __init__(self, step_index, watch_info, expected_value):
+ self.step_index = step_index
+ self.watch_info = watch_info
+ self.expected_value = expected_value
+
+ def __str__(self):
+ return '{}:{}: expected value:{}'.format(self.step_index, self.watch_info, self.expected_value)
+
+ def __eq__(self, other):
+ return (self.watch_info.expression == other.watch_info.expression
+ and self.expected_value == other.expected_value)
+
+ def __hash__(self):
+ return hash(self.watch_info.expression, self.expected_value)
diff --git a/debuginfo-tests/dexter/dex/command/__init__.py b/debuginfo-tests/dexter/dex/command/__init__.py
new file mode 100644
index 00000000000..70da546fe5e
--- /dev/null
+++ b/debuginfo-tests/dexter/dex/command/__init__.py
@@ -0,0 +1,9 @@
+# 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
+
+from dex.command.ParseCommand import find_all_commands
+from dex.command.StepValueInfo import StepValueInfo
diff --git a/debuginfo-tests/dexter/dex/command/commands/DexExpectProgramState.py b/debuginfo-tests/dexter/dex/command/commands/DexExpectProgramState.py
new file mode 100644
index 00000000000..78335838a65
--- /dev/null
+++ b/debuginfo-tests/dexter/dex/command/commands/DexExpectProgramState.py
@@ -0,0 +1,83 @@
+# 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
+"""Command for specifying a partial or complete state for the program to enter
+during execution.
+"""
+
+from itertools import chain
+
+from dex.command.CommandBase import CommandBase
+from dex.dextIR import ProgramState, SourceLocation, StackFrame, DextIR
+
+def frame_from_dict(source: dict) -> StackFrame:
+ if 'location' in source:
+ assert isinstance(source['location'], dict)
+ source['location'] = SourceLocation(**source['location'])
+ return StackFrame(**source)
+
+def state_from_dict(source: dict) -> ProgramState:
+ if 'frames' in source:
+ assert isinstance(source['frames'], list)
+ source['frames'] = list(map(frame_from_dict, source['frames']))
+ return ProgramState(**source)
+
+class DexExpectProgramState(CommandBase):
+ """Expect to see a given program `state` a certain numer of `times`.
+
+ DexExpectProgramState(state [,**times])
+
+ See Commands.md for more info.
+ """
+
+ def __init__(self, *args, **kwargs):
+ if len(args) != 1:
+ raise TypeError('expected exactly one unnamed arg')
+
+ self.program_state_text = str(args[0])
+
+ self.expected_program_state = state_from_dict(args[0])
+
+ self.times = kwargs.pop('times', -1)
+ if kwargs:
+ raise TypeError('unexpected named args: {}'.format(
+ ', '.join(kwargs)))
+
+ # Step indices at which the expected program state was encountered.
+ self.encounters = []
+
+ super(DexExpectProgramState, self).__init__()
+
+ @staticmethod
+ def get_name():
+ return __class__.__name__
+
+ def get_watches(self):
+ frame_expects = chain.from_iterable(frame.watches
+ for frame in self.expected_program_state.frames)
+ return set(frame_expects)
+
+ def eval(self, step_collection: DextIR) -> bool:
+ for step in step_collection.steps:
+ if self.expected_program_state.match(step.program_state):
+ self.encounters.append(step.step_index)
+
+ return self.times < 0 < len(self.encounters) or len(self.encounters) == self.times
+
+ def has_labels(self):
+ return len(self.get_label_args()) > 0
+
+ def get_label_args(self):
+ return [frame.location.lineno
+ for frame in self.expected_program_state.frames
+ if frame.location and
+ isinstance(frame.location.lineno, str)]
+
+ def resolve_label(self, label_line__pair):
+ label, line = label_line__pair
+ for frame in self.expected_program_state.frames:
+ if frame.location and frame.location.lineno == label:
+ frame.location.lineno = line
diff --git a/debuginfo-tests/dexter/dex/command/commands/DexExpectStepKind.py b/debuginfo-tests/dexter/dex/command/commands/DexExpectStepKind.py
new file mode 100644
index 00000000000..6370f5d32c7
--- /dev/null
+++ b/debuginfo-tests/dexter/dex/command/commands/DexExpectStepKind.py
@@ -0,0 +1,45 @@
+# 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
+"""Command for specifying an expected number of steps of a particular kind."""
+
+from dex.command.CommandBase import CommandBase
+from dex.dextIR.StepIR import StepKind
+
+
+class DexExpectStepKind(CommandBase):
+ """Expect to see a particular step `kind` a number of `times` while stepping
+ through the program.
+
+ DexExpectStepKind(kind, times)
+
+ See Commands.md for more info.
+ """
+
+ def __init__(self, *args):
+ if len(args) != 2:
+ raise TypeError('expected two args')
+
+ try:
+ step_kind = StepKind[args[0]]
+ except KeyError:
+ raise TypeError('expected arg 0 to be one of {}'.format(
+ [kind for kind, _ in StepKind.__members__.items()]))
+
+ self.name = step_kind
+ self.count = args[1]
+
+ super(DexExpectStepKind, self).__init__()
+
+ @staticmethod
+ def get_name():
+ return __class__.__name__
+
+ def eval(self):
+ # DexExpectStepKind eval() implementation is mixed into
+ # Heuristic.__init__()
+ # [TODO] Fix this ^.
+ pass
diff --git a/debuginfo-tests/dexter/dex/command/commands/DexExpectStepOrder.py b/debuginfo-tests/dexter/dex/command/commands/DexExpectStepOrder.py
new file mode 100644
index 00000000000..4342bc5e80b
--- /dev/null
+++ b/debuginfo-tests/dexter/dex/command/commands/DexExpectStepOrder.py
@@ -0,0 +1,39 @@
+# 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
+
+from dex.command.CommandBase import CommandBase
+from dex.dextIR import ValueIR
+
+class DexExpectStepOrder(CommandBase):
+ """Expect the line every `DexExpectStepOrder` is found on to be stepped on
+ in `order`. Each instance must have a set of unique ascending indicies.
+
+ DexExpectStepOrder(*order)
+
+ See Commands.md for more info.
+ """
+
+ def __init__(self, *args):
+ if not args:
+ raise TypeError('Need at least one order number')
+
+ self.sequence = [int(x) for x in args]
+ super(DexExpectStepOrder, self).__init__()
+
+ @staticmethod
+ def get_name():
+ return __class__.__name__
+
+ def eval(self, debugger):
+ step_info = debugger.get_step_info()
+ loc = step_info.current_location
+ return {'DexExpectStepOrder': ValueIR(expression=str(loc.lineno),
+ value=str(debugger.step_index), type_name=None,
+ error_string=None,
+ could_evaluate=True,
+ is_optimized_away=True,
+ is_irretrievable=False)}
diff --git a/debuginfo-tests/dexter/dex/command/commands/DexExpectWatchBase.py b/debuginfo-tests/dexter/dex/command/commands/DexExpectWatchBase.py
new file mode 100644
index 00000000000..e6422d14098
--- /dev/null
+++ b/debuginfo-tests/dexter/dex/command/commands/DexExpectWatchBase.py
@@ -0,0 +1,197 @@
+# 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
+
+from dex.command.CommandBase import CommandBase
+from dex.command.StepValueInfo import StepValueInfo
+
+
+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 = [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 = []
+
+ super(DexExpectWatchBase, self).__init__()
+
+
+ def get_watches(self):
+ return [self.expression]
+
+ @property
+ def line_range(self):
+ return list(range(self._from_line, self._to_line + 1))
+
+ @property
+ def missing_values(self):
+ return sorted(list(self._missing_values))
+
+ @property
+ def encountered_values(self):
+ return sorted(list(set(self.values) - self._missing_values))
+
+
+ def resolve_label(self, label_line_pair):
+ # from_line and to_line could have the same label.
+ label, lineno = label_line_pair
+ if self._to_line == label:
+ self._to_line = lineno
+ if self._from_line == label:
+ self._from_line = lineno
+
+ def has_labels(self):
+ return len(self.get_label_args()) > 0
+
+ def get_label_args(self):
+ return [label for label in (self._from_line, self._to_line)
+ if isinstance(label, str)]
+
+ @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
+
+ if step_info.expected_value not in self.values:
+ self.unexpected_watches.append(step_info)
+ return
+
+ self.expected_watches.append(step_info)
+ try:
+ self._missing_values.remove(step_info.expected_value)
+ 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):
+ for step in step_collection.steps:
+ loc = step.current_location
+
+ if (os.path.exists(loc.path) and os.path.exists(self.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
+
+ self.misordered_watches = self._check_watch_order(
+ value_change_watches, [
+ v for v in self.values if v in
+ [w.expected_value for w in self.expected_watches]
+ ])
diff --git a/debuginfo-tests/dexter/dex/command/commands/DexExpectWatchType.py b/debuginfo-tests/dexter/dex/command/commands/DexExpectWatchType.py
new file mode 100644
index 00000000000..f2336de4828
--- /dev/null
+++ b/debuginfo-tests/dexter/dex/command/commands/DexExpectWatchType.py
@@ -0,0 +1,26 @@
+# 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
+"""Command for specifying an expected set of types for a particular watch."""
+
+
+from dex.command.commands.DexExpectWatchBase import DexExpectWatchBase
+
+class DexExpectWatchType(DexExpectWatchBase):
+ """Expect the expression `expr` to evaluate be evaluated and have each
+ evaluation's type checked against the list of `types`.
+
+ DexExpectWatchType(expr, *types [,**from_line=1][,**to_line=Max]
+ [,**on_line])
+
+ See Commands.md for more info.
+ """
+ @staticmethod
+ def get_name():
+ return __class__.__name__
+
+ def _get_expected_field(self, watch):
+ return watch.type_name
diff --git a/debuginfo-tests/dexter/dex/command/commands/DexExpectWatchValue.py b/debuginfo-tests/dexter/dex/command/commands/DexExpectWatchValue.py
new file mode 100644
index 00000000000..d6da006ee8c
--- /dev/null
+++ b/debuginfo-tests/dexter/dex/command/commands/DexExpectWatchValue.py
@@ -0,0 +1,27 @@
+# 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
+"""Command for specifying an expected set of values for a particular watch."""
+
+
+from dex.command.commands.DexExpectWatchBase import DexExpectWatchBase
+
+class DexExpectWatchValue(DexExpectWatchBase):
+ """Expect the expression `expr` to evaluate to the list of `values`
+ sequentially.
+
+ DexExpectWatchValue(expr, *values [,**from_line=1][,**to_line=Max]
+ [,**on_line])
+
+ See Commands.md for more info.
+ """
+
+ @staticmethod
+ def get_name():
+ return __class__.__name__
+
+ def _get_expected_field(self, watch):
+ return watch.value
diff --git a/debuginfo-tests/dexter/dex/command/commands/DexLabel.py b/debuginfo-tests/dexter/dex/command/commands/DexLabel.py
new file mode 100644
index 00000000000..8a0325e6634
--- /dev/null
+++ b/debuginfo-tests/dexter/dex/command/commands/DexLabel.py
@@ -0,0 +1,31 @@
+# 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
+"""Command used to give a line in a test a named psuedonym. Every DexLabel has
+ a line number and Label string component.
+"""
+
+from dex.command.CommandBase import CommandBase
+
+
+class DexLabel(CommandBase):
+ def __init__(self, label):
+
+ if not isinstance(label, str):
+ raise TypeError('invalid argument type')
+
+ self._label = label
+ super(DexLabel, self).__init__()
+
+ def get_as_pair(self):
+ return (self._label, self.lineno)
+
+ @staticmethod
+ def get_name():
+ return __class__.__name__
+
+ def eval(self):
+ return self._label
diff --git a/debuginfo-tests/dexter/dex/command/commands/DexUnreachable.py b/debuginfo-tests/dexter/dex/command/commands/DexUnreachable.py
new file mode 100644
index 00000000000..188a5d8180d
--- /dev/null
+++ b/debuginfo-tests/dexter/dex/command/commands/DexUnreachable.py
@@ -0,0 +1,38 @@
+# 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
+
+
+from dex.command.CommandBase import CommandBase
+from dex.dextIR import ValueIR
+
+
+class DexUnreachable(CommandBase):
+ """Expect the source line this is found on will never be stepped on to.
+
+ DexUnreachable()
+
+ See Commands.md for more info.
+ """
+
+ def __init(self):
+ super(DexUnreachable, self).__init__()
+ pass
+
+ @staticmethod
+ def get_name():
+ return __class__.__name__
+
+ def eval(self, debugger):
+ # If we're ever called, at all, then we're evaluating a line that has
+ # been marked as unreachable. Which means a failure.
+ vir = ValueIR(expression="Unreachable",
+ value="True", type_name=None,
+ error_string=None,
+ could_evaluate=True,
+ is_optimized_away=True,
+ is_irretrievable=False)
+ return {'DexUnreachable' : vir}
diff --git a/debuginfo-tests/dexter/dex/command/commands/DexWatch.py b/debuginfo-tests/dexter/dex/command/commands/DexWatch.py
new file mode 100644
index 00000000000..2dfa3a36fb3
--- /dev/null
+++ b/debuginfo-tests/dexter/dex/command/commands/DexWatch.py
@@ -0,0 +1,39 @@
+# 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
+"""Command to instruct the debugger to inspect the value of some set of
+expressions on the current source line.
+"""
+
+from dex.command.CommandBase import CommandBase
+
+
+class DexWatch(CommandBase):
+ """[Deprecated] Evaluate each given `expression` when the debugger steps onto the
+ line this command is found on
+
+ DexWatch(*expressions)
+
+ See Commands.md for more info.
+ """
+
+ def __init__(self, *args):
+ if not args:
+ raise TypeError('expected some arguments')
+
+ for arg in args:
+ if not isinstance(arg, str):
+ raise TypeError('invalid argument type')
+
+ self._args = args
+ super(DexWatch, self).__init__()
+
+ @staticmethod
+ def get_name():
+ return __class__.__name__
+
+ def eval(self, debugger):
+ return {arg: debugger.evaluate_expression(arg) for arg in self._args}
OpenPOWER on IntegriCloud