diff options
Diffstat (limited to 'debuginfo-tests/dexter/dex/command')
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} |