diff options
author | Jeremy Morse <jeremy.morse@sony.com> | 2019-10-31 16:51:53 +0000 |
---|---|---|
committer | Jeremy Morse <jeremy.morse@sony.com> | 2019-10-31 16:51:53 +0000 |
commit | 984fad243d179564df31c5f9531a52442e24581a (patch) | |
tree | aba85a27f1596d456079f6f5eb69e09408730b49 /debuginfo-tests/dexter/dex | |
parent | 34f3c0fc44a5fd8a0f9186002749336e398837cf (diff) | |
download | bcm5719-llvm-984fad243d179564df31c5f9531a52442e24581a.tar.gz bcm5719-llvm-984fad243d179564df31c5f9531a52442e24581a.zip |
Reapply "Import Dexter to debuginfo-tests""
This reverts commit cb935f345683194e42e6e883d79c5a16479acd74.
Discussion in D68708 advises that green dragon is being briskly
refurbished, and it's good to have this patch up testing it.
Diffstat (limited to 'debuginfo-tests/dexter/dex')
89 files changed, 7918 insertions, 0 deletions
diff --git a/debuginfo-tests/dexter/dex/__init__.py b/debuginfo-tests/dexter/dex/__init__.py new file mode 100644 index 00000000000..d2a290b0ee0 --- /dev/null +++ b/debuginfo-tests/dexter/dex/__init__.py @@ -0,0 +1,8 @@ +# 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 + +__version__ = '1.0.0' diff --git a/debuginfo-tests/dexter/dex/builder/Builder.py b/debuginfo-tests/dexter/dex/builder/Builder.py new file mode 100644 index 00000000000..a2bab863568 --- /dev/null +++ b/debuginfo-tests/dexter/dex/builder/Builder.py @@ -0,0 +1,117 @@ +# 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 +"""Deals with the processing execution of shell or batch build scripts.""" + +import os +import subprocess +import unittest + +from dex.dextIR import BuilderIR +from dex.utils import Timer +from dex.utils.Exceptions import BuildScriptException + + +def _quotify(text): + if '"' in text or ' ' not in text: + return text + return '"{}"'.format(text) + + +def _get_script_environment(source_files, compiler_options, + linker_options, executable_file): + + source_files = [_quotify(f) for f in source_files] + object_files = [ + _quotify('{}.o'.format(os.path.basename(f))) for f in source_files + ] + source_indexes = ['{:02d}'.format(i + 1) for i in range(len(source_files))] + + env_variables = {} + env_variables['SOURCE_INDEXES'] = ' '.join(source_indexes) + env_variables['SOURCE_FILES'] = ' '.join(source_files) + env_variables['OBJECT_FILES'] = ' '.join(object_files) + env_variables['LINKER_OPTIONS'] = linker_options + + for i, _ in enumerate(source_files): + index = source_indexes[i] + env_variables['SOURCE_FILE_{}'.format(index)] = source_files[i] + env_variables['OBJECT_FILE_{}'.format(index)] = object_files[i] + env_variables['COMPILER_OPTIONS_{}'.format(index)] = compiler_options[i] + + env_variables['EXECUTABLE_FILE'] = executable_file + + return env_variables + + +def run_external_build_script(context, script_path, source_files, + compiler_options, linker_options, + executable_file): + """Build an executable using a builder script. + + The executable is saved to `context.working_directory.path`. + + Returns: + ( stdout (str), stderr (str), builder (BuilderIR) ) + """ + + builderIR = BuilderIR( + name=context.options.builder, + cflags=compiler_options, + ldflags=linker_options, + ) + assert len(source_files) == len(compiler_options), (source_files, + compiler_options) + + script_environ = _get_script_environment(source_files, compiler_options, + linker_options, executable_file) + env = dict(os.environ) + env.update(script_environ) + try: + with Timer('running build script'): + process = subprocess.Popen( + [script_path], + cwd=context.working_directory.path, + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + out, err = process.communicate() + returncode = process.returncode + if returncode != 0: + raise BuildScriptException( + '{}: failed with returncode {}.\nstdout:\n{}\n\nstderr:\n{}\n'. + format(script_path, returncode, out, err), + script_error=err) + return out.decode('utf-8'), err.decode('utf-8'), builderIR + except OSError as e: + raise BuildScriptException('{}: {}'.format(e.strerror, script_path)) + + +class TestBuilder(unittest.TestCase): + def test_get_script_environment(self): + source_files = ['a.a', 'b.b'] + compiler_options = ['-option1 value1', '-option2 value2'] + linker_options = '-optionX valueX' + executable_file = 'exe.exe' + env = _get_script_environment(source_files, compiler_options, + linker_options, executable_file) + + assert env['SOURCE_FILES'] == 'a.a b.b' + assert env['OBJECT_FILES'] == 'a.a.o b.b.o' + + assert env['SOURCE_INDEXES'] == '01 02' + assert env['LINKER_OPTIONS'] == '-optionX valueX' + + assert env['SOURCE_FILE_01'] == 'a.a' + assert env['SOURCE_FILE_02'] == 'b.b' + + assert env['OBJECT_FILE_01'] == 'a.a.o' + assert env['OBJECT_FILE_02'] == 'b.b.o' + + assert env['EXECUTABLE_FILE'] == 'exe.exe' + + assert env['COMPILER_OPTIONS_01'] == '-option1 value1' + assert env['COMPILER_OPTIONS_02'] == '-option2 value2' diff --git a/debuginfo-tests/dexter/dex/builder/ParserOptions.py b/debuginfo-tests/dexter/dex/builder/ParserOptions.py new file mode 100644 index 00000000000..b6677aac549 --- /dev/null +++ b/debuginfo-tests/dexter/dex/builder/ParserOptions.py @@ -0,0 +1,56 @@ +# 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 line options for subtools that use the builder component.""" + +import os + +from dex.tools import Context +from dex.utils import is_native_windows + + +def _find_build_scripts(): + """Finds build scripts in the 'scripts' subdirectory. + + Returns: + { script_name (str): directory (str) } + """ + try: + return _find_build_scripts.cached + except AttributeError: + scripts_directory = os.path.join(os.path.dirname(__file__), 'scripts') + if is_native_windows(): + scripts_directory = os.path.join(scripts_directory, 'windows') + else: + scripts_directory = os.path.join(scripts_directory, 'posix') + assert os.path.isdir(scripts_directory), scripts_directory + results = {} + + for f in os.listdir(scripts_directory): + results[os.path.splitext(f)[0]] = os.path.abspath( + os.path.join(scripts_directory, f)) + + _find_build_scripts.cached = results + return results + + +def add_builder_tool_arguments(parser): + parser.add_argument('--binary', + metavar="<file>", + help='provide binary file to override --builder') + + parser.add_argument( + '--builder', + type=str, + choices=sorted(_find_build_scripts().keys()), + help='test builder to use') + parser.add_argument( + '--cflags', type=str, default='', help='compiler flags') + parser.add_argument('--ldflags', type=str, default='', help='linker flags') + + +def handle_builder_tool_options(context: Context) -> str: + return _find_build_scripts()[context.options.builder] diff --git a/debuginfo-tests/dexter/dex/builder/__init__.py b/debuginfo-tests/dexter/dex/builder/__init__.py new file mode 100644 index 00000000000..3bf0ca40f5c --- /dev/null +++ b/debuginfo-tests/dexter/dex/builder/__init__.py @@ -0,0 +1,10 @@ +# 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.builder.Builder import run_external_build_script +from dex.builder.ParserOptions import add_builder_tool_arguments +from dex.builder.ParserOptions import handle_builder_tool_options diff --git a/debuginfo-tests/dexter/dex/builder/scripts/posix/clang-c.sh b/debuginfo-tests/dexter/dex/builder/scripts/posix/clang-c.sh new file mode 100755 index 00000000000..f69f51cd86a --- /dev/null +++ b/debuginfo-tests/dexter/dex/builder/scripts/posix/clang-c.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -e + +if test -z "$PATHTOCLANG"; then + PATHTOCLANG=clang +fi + +for INDEX in $SOURCE_INDEXES +do + CFLAGS=$(eval echo "\$COMPILER_OPTIONS_$INDEX") + SRCFILE=$(eval echo "\$SOURCE_FILE_$INDEX") + OBJFILE=$(eval echo "\$OBJECT_FILE_$INDEX") + $PATHTOCLANG -std=gnu11 -c $CFLAGS $SRCFILE -o $OBJFILE +done + +$PATHTOCLANG $LINKER_OPTIONS $OBJECT_FILES -o $EXECUTABLE_FILE diff --git a/debuginfo-tests/dexter/dex/builder/scripts/posix/clang.sh b/debuginfo-tests/dexter/dex/builder/scripts/posix/clang.sh new file mode 100755 index 00000000000..9cf4cdd65f7 --- /dev/null +++ b/debuginfo-tests/dexter/dex/builder/scripts/posix/clang.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -e + +if test -z "$PATHTOCLANGPP"; then + PATHTOCLANGPP=clang++ +fi + +for INDEX in $SOURCE_INDEXES +do + CFLAGS=$(eval echo "\$COMPILER_OPTIONS_$INDEX") + SRCFILE=$(eval echo "\$SOURCE_FILE_$INDEX") + OBJFILE=$(eval echo "\$OBJECT_FILE_$INDEX") + $PATHTOCLANGPP -std=gnu++11 -c $CFLAGS $SRCFILE -o $OBJFILE +done + +$PATHTOCLANGPP $LINKER_OPTIONS $OBJECT_FILES -o $EXECUTABLE_FILE diff --git a/debuginfo-tests/dexter/dex/builder/scripts/windows/clang-cl_vs2015.bat b/debuginfo-tests/dexter/dex/builder/scripts/windows/clang-cl_vs2015.bat new file mode 100644 index 00000000000..ea0d4414d26 --- /dev/null +++ b/debuginfo-tests/dexter/dex/builder/scripts/windows/clang-cl_vs2015.bat @@ -0,0 +1,23 @@ +@echo OFF +setlocal EnableDelayedExpansion + +call "%VS140COMNTOOLS%..\..\VC\bin\amd64\vcvars64.bat" + +@echo OFF +setlocal EnableDelayedExpansion + +for %%I in (%SOURCE_INDEXES%) do ( + %PATHTOCLANGCL% /c !COMPILER_OPTIONS_%%I! !SOURCE_FILE_%%I! /Fo!OBJECT_FILE_%%I! + if errorlevel 1 goto :FAIL +) + +%PATHTOCLANGCL% %LINKER_OPTIONS% %OBJECT_FILES% /Fe%EXECUTABLE_FILE% +if errorlevel 1 goto :FAIL +goto :END + +:FAIL +echo FAILED +exit /B 1 + +:END +exit /B 0 diff --git a/debuginfo-tests/dexter/dex/builder/scripts/windows/clang.bat b/debuginfo-tests/dexter/dex/builder/scripts/windows/clang.bat new file mode 100644 index 00000000000..a83e4d4c1bb --- /dev/null +++ b/debuginfo-tests/dexter/dex/builder/scripts/windows/clang.bat @@ -0,0 +1,17 @@ +setlocal EnableDelayedExpansion + +for %%I in (%SOURCE_INDEXES%) do ( + %PATHTOCLANGPP% -fuse-ld=lld -c !COMPILER_OPTIONS_%%I! !SOURCE_FILE_%%I! -o !OBJECT_FILE_%%I! + if errorlevel 1 goto :FAIL +) + +%PATHTOCLANGPP% -fuse-ld=lld %LINKER_OPTIONS% %OBJECT_FILES% -o %EXECUTABLE_FILE% +if errorlevel 1 goto :FAIL +goto :END + +:FAIL +echo FAILED +exit /B 1 + +:END +exit /B 0 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} diff --git a/debuginfo-tests/dexter/dex/debugger/DebuggerBase.py b/debuginfo-tests/dexter/dex/debugger/DebuggerBase.py new file mode 100644 index 00000000000..8013ceb6436 --- /dev/null +++ b/debuginfo-tests/dexter/dex/debugger/DebuggerBase.py @@ -0,0 +1,227 @@ +# 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 debugger interface implementations.""" + +import abc +from itertools import chain +import os +import sys +import time +import traceback + +from dex.dextIR import DebuggerIR, ValueIR +from dex.utils.Exceptions import DebuggerException +from dex.utils.Exceptions import NotYetLoadedDebuggerException +from dex.utils.ReturnCode import ReturnCode + + +class DebuggerBase(object, metaclass=abc.ABCMeta): + def __init__(self, context, step_collection): + self.context = context + self.steps = step_collection + self._interface = None + self.has_loaded = False + self._loading_error = NotYetLoadedDebuggerException() + self.watches = set() + + try: + self._interface = self._load_interface() + self.has_loaded = True + self._loading_error = None + except DebuggerException: + self._loading_error = sys.exc_info() + + self.step_index = 0 + + def __enter__(self): + try: + self._custom_init() + self.clear_breakpoints() + self.add_breakpoints() + except DebuggerException: + self._loading_error = sys.exc_info() + return self + + def __exit__(self, *args): + self._custom_exit() + + def _custom_init(self): + pass + + def _custom_exit(self): + pass + + @property + def debugger_info(self): + return DebuggerIR(name=self.name, version=self.version) + + @property + def is_available(self): + return self.has_loaded and self.loading_error is None + + @property + def loading_error(self): + return (str(self._loading_error[1]) + if self._loading_error is not None else None) + + @property + def loading_error_trace(self): + if not self._loading_error: + return None + + tb = traceback.format_exception(*self._loading_error) + + if self._loading_error[1].orig_exception is not None: + orig_exception = traceback.format_exception( + *self._loading_error[1].orig_exception) + + if ''.join(orig_exception) not in ''.join(tb): + tb.extend(['\n']) + tb.extend(orig_exception) + + tb = ''.join(tb).splitlines(True) + return tb + + def add_breakpoints(self): + for s in self.context.options.source_files: + with open(s, 'r') as fp: + num_lines = len(fp.readlines()) + for line in range(1, num_lines + 1): + self.add_breakpoint(s, line) + + def _update_step_watches(self, step_info): + loc = step_info.current_location + watch_cmds = ['DexUnreachable', 'DexExpectStepOrder'] + towatch = chain.from_iterable(self.steps.commands[x] + for x in watch_cmds + if x in self.steps.commands) + try: + # Iterate over all watches of the types named in watch_cmds + for watch in towatch: + if (os.path.exists(loc.path) + and os.path.samefile(watch.path, loc.path) + and watch.lineno == loc.lineno): + result = watch.eval(self) + step_info.watches.update(result) + break + except KeyError: + pass + + def _sanitize_function_name(self, name): # pylint: disable=no-self-use + """If the function name returned by the debugger needs any post- + processing to make it fit (for example, if it includes a byte offset), + do that here. + """ + return name + + def start(self): + self.steps.clear_steps() + self.launch() + + for command_obj in chain.from_iterable(self.steps.commands.values()): + self.watches.update(command_obj.get_watches()) + + max_steps = self.context.options.max_steps + for _ in range(max_steps): + while self.is_running: + pass + + if self.is_finished: + break + + self.step_index += 1 + step_info = self.get_step_info() + + if step_info.current_frame: + self._update_step_watches(step_info) + self.steps.new_step(self.context, step_info) + + if self.in_source_file(step_info): + self.step() + else: + self.go() + + time.sleep(self.context.options.pause_between_steps) + else: + raise DebuggerException( + 'maximum number of steps reached ({})'.format(max_steps)) + + def in_source_file(self, step_info): + if not step_info.current_frame: + return False + if not os.path.exists(step_info.current_location.path): + return False + return any(os.path.samefile(step_info.current_location.path, f) \ + for f in self.context.options.source_files) + + @abc.abstractmethod + def _load_interface(self): + pass + + @classmethod + def get_option_name(cls): + """Short name that will be used on the command line to specify this + debugger. + """ + raise NotImplementedError() + + @classmethod + def get_name(cls): + """Full name of this debugger.""" + raise NotImplementedError() + + @property + def name(self): + return self.__class__.get_name() + + @property + def option_name(self): + return self.__class__.get_option_name() + + @abc.abstractproperty + def version(self): + pass + + @abc.abstractmethod + def clear_breakpoints(self): + pass + + @abc.abstractmethod + def add_breakpoint(self, file_, line): + pass + + @abc.abstractmethod + def launch(self): + pass + + @abc.abstractmethod + def step(self): + pass + + @abc.abstractmethod + def go(self) -> ReturnCode: + pass + + @abc.abstractmethod + def get_step_info(self): + pass + + @abc.abstractproperty + def is_running(self): + pass + + @abc.abstractproperty + def is_finished(self): + pass + + @abc.abstractproperty + def frames_below_main(self): + pass + + @abc.abstractmethod + def evaluate_expression(self, expression, frame_idx=0) -> ValueIR: + pass diff --git a/debuginfo-tests/dexter/dex/debugger/Debuggers.py b/debuginfo-tests/dexter/dex/debugger/Debuggers.py new file mode 100644 index 00000000000..2f246a3fd95 --- /dev/null +++ b/debuginfo-tests/dexter/dex/debugger/Debuggers.py @@ -0,0 +1,299 @@ +# 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 +"""Discover potential/available debugger interfaces.""" + +from collections import OrderedDict +import os +import pickle +import subprocess +import sys +from tempfile import NamedTemporaryFile + +from dex.command import find_all_commands +from dex.dextIR import DextIR +from dex.utils import get_root_directory, Timer +from dex.utils.Environment import is_native_windows +from dex.utils.Exceptions import CommandParseError, DebuggerException +from dex.utils.Exceptions import ToolArgumentError +from dex.utils.Warning import warn + +from dex.debugger.dbgeng.dbgeng import DbgEng +from dex.debugger.lldb.LLDB import LLDB +from dex.debugger.visualstudio.VisualStudio2015 import VisualStudio2015 +from dex.debugger.visualstudio.VisualStudio2017 import VisualStudio2017 + + +def _get_potential_debuggers(): # noqa + """Return a dict of the supported debuggers. + Returns: + { name (str): debugger (class) } + """ + return { + DbgEng.get_option_name(): DbgEng, + LLDB.get_option_name(): LLDB, + VisualStudio2015.get_option_name(): VisualStudio2015, + VisualStudio2017.get_option_name(): VisualStudio2017 + } + + +def _warn_meaningless_option(context, option): + if context.options.list_debuggers: + return + + warn(context, + 'option <y>"{}"</> is meaningless with this debugger'.format(option), + '--debugger={}'.format(context.options.debugger)) + + +def add_debugger_tool_base_arguments(parser, defaults): + defaults.lldb_executable = 'lldb.exe' if is_native_windows() else 'lldb' + parser.add_argument( + '--lldb-executable', + type=str, + metavar='<file>', + default=None, + display_default=defaults.lldb_executable, + help='location of LLDB executable') + + +def add_debugger_tool_arguments(parser, context, defaults): + debuggers = Debuggers(context) + potential_debuggers = sorted(debuggers.potential_debuggers().keys()) + + add_debugger_tool_base_arguments(parser, defaults) + + parser.add_argument( + '--debugger', + type=str, + choices=potential_debuggers, + required=True, + help='debugger to use') + parser.add_argument( + '--max-steps', + metavar='<int>', + type=int, + default=1000, + help='maximum number of program steps allowed') + parser.add_argument( + '--pause-between-steps', + metavar='<seconds>', + type=float, + default=0.0, + help='number of seconds to pause between steps') + defaults.show_debugger = False + parser.add_argument( + '--show-debugger', + action='store_true', + default=None, + help='show the debugger') + defaults.arch = 'x86_64' + parser.add_argument( + '--arch', + type=str, + metavar='<architecture>', + default=None, + display_default=defaults.arch, + help='target architecture') + + +def handle_debugger_tool_base_options(context, defaults): # noqa + options = context.options + + if options.lldb_executable is None: + options.lldb_executable = defaults.lldb_executable + else: + if getattr(options, 'debugger', 'lldb') != 'lldb': + _warn_meaningless_option(context, '--lldb-executable') + + options.lldb_executable = os.path.abspath(options.lldb_executable) + if not os.path.isfile(options.lldb_executable): + raise ToolArgumentError('<d>could not find</> <r>"{}"</>'.format( + options.lldb_executable)) + + +def handle_debugger_tool_options(context, defaults): # noqa + options = context.options + + handle_debugger_tool_base_options(context, defaults) + + if options.arch is None: + options.arch = defaults.arch + else: + if options.debugger != 'lldb': + _warn_meaningless_option(context, '--arch') + + if options.show_debugger is None: + options.show_debugger = defaults.show_debugger + else: + if options.debugger == 'lldb': + _warn_meaningless_option(context, '--show-debugger') + + +def _get_command_infos(context): + commands = find_all_commands(context.options.source_files) + command_infos = OrderedDict() + for command_type in commands: + for command in commands[command_type].values(): + if command_type not in command_infos: + command_infos[command_type] = [] + command_infos[command_type].append(command) + return OrderedDict(command_infos) + + +def empty_debugger_steps(context): + return DextIR( + executable_path=context.options.executable, + source_paths=context.options.source_files, + dexter_version=context.version) + + +def get_debugger_steps(context): + step_collection = empty_debugger_steps(context) + + with Timer('parsing commands'): + try: + step_collection.commands = _get_command_infos(context) + except CommandParseError as e: + msg = 'parser error: <d>{}({}):</> {}\n{}\n{}\n'.format( + e.filename, e.lineno, e.info, e.src, e.caret) + raise DebuggerException(msg) + + with NamedTemporaryFile( + dir=context.working_directory.path, delete=False) as fp: + pickle.dump(step_collection, fp, protocol=pickle.HIGHEST_PROTOCOL) + steps_path = fp.name + + with NamedTemporaryFile( + dir=context.working_directory.path, delete=False, mode='wb') as fp: + pickle.dump(context.options, fp, protocol=pickle.HIGHEST_PROTOCOL) + options_path = fp.name + + dexter_py = os.path.basename(sys.argv[0]) + if not os.path.isfile(dexter_py): + dexter_py = os.path.join(get_root_directory(), '..', dexter_py) + assert os.path.isfile(dexter_py) + + with NamedTemporaryFile(dir=context.working_directory.path) as fp: + args = [ + sys.executable, dexter_py, 'run-debugger-internal-', steps_path, + options_path, '--working-directory', context.working_directory.path, + '--unittest=off', '--indent-timer-level={}'.format(Timer.indent + 2) + ] + try: + with Timer('running external debugger process'): + subprocess.check_call(args) + except subprocess.CalledProcessError as e: + raise DebuggerException(e) + + with open(steps_path, 'rb') as fp: + step_collection = pickle.load(fp) + + return step_collection + + +class Debuggers(object): + @classmethod + def potential_debuggers(cls): + try: + return cls._potential_debuggers + except AttributeError: + cls._potential_debuggers = _get_potential_debuggers() + return cls._potential_debuggers + + def __init__(self, context): + self.context = context + + def load(self, key, step_collection=None): + with Timer('load {}'.format(key)): + return Debuggers.potential_debuggers()[key](self.context, + step_collection) + + def _populate_debugger_cache(self): + debuggers = [] + for key in sorted(Debuggers.potential_debuggers()): + debugger = self.load(key) + + class LoadedDebugger(object): + pass + + LoadedDebugger.option_name = key + LoadedDebugger.full_name = '[{}]'.format(debugger.name) + LoadedDebugger.is_available = debugger.is_available + + if LoadedDebugger.is_available: + try: + LoadedDebugger.version = debugger.version.splitlines() + except AttributeError: + LoadedDebugger.version = [''] + else: + try: + LoadedDebugger.error = debugger.loading_error.splitlines() + except AttributeError: + LoadedDebugger.error = [''] + + try: + LoadedDebugger.error_trace = debugger.loading_error_trace + except AttributeError: + LoadedDebugger.error_trace = None + + debuggers.append(LoadedDebugger) + return debuggers + + def list(self): + debuggers = self._populate_debugger_cache() + + max_o_len = max(len(d.option_name) for d in debuggers) + max_n_len = max(len(d.full_name) for d in debuggers) + + msgs = [] + + for d in debuggers: + # Option name, right padded with spaces for alignment + option_name = ( + '{{name: <{}}}'.format(max_o_len).format(name=d.option_name)) + + # Full name, right padded with spaces for alignment + full_name = ('{{name: <{}}}'.format(max_n_len) + .format(name=d.full_name)) + + if d.is_available: + name = '<b>{} {}</>'.format(option_name, full_name) + + # If the debugger is available, show the first line of the + # version info. + available = '<g>YES</>' + info = '<b>({})</>'.format(d.version[0]) + else: + name = '<y>{} {}</>'.format(option_name, full_name) + + # If the debugger is not available, show the first line of the + # error reason. + available = '<r>NO</> ' + info = '<y>({})</>'.format(d.error[0]) + + msg = '{} {} {}'.format(name, available, info) + + if self.context.options.verbose: + # If verbose mode and there was more version or error output + # than could be displayed in a single line, display the whole + # lot slightly indented. + verbose_info = None + if d.is_available: + if d.version[1:]: + verbose_info = d.version + ['\n'] + else: + # Some of list elems may contain multiple lines, so make + # sure each elem is a line of its own. + verbose_info = d.error_trace + + if verbose_info: + verbose_info = '\n'.join(' {}'.format(l.rstrip()) + for l in verbose_info) + '\n' + msg = '{}\n\n{}'.format(msg, verbose_info) + + msgs.append(msg) + self.context.o.auto('\n{}\n\n'.format('\n'.join(msgs))) diff --git a/debuginfo-tests/dexter/dex/debugger/__init__.py b/debuginfo-tests/dexter/dex/debugger/__init__.py new file mode 100644 index 00000000000..3c4fdece479 --- /dev/null +++ b/debuginfo-tests/dexter/dex/debugger/__init__.py @@ -0,0 +1,8 @@ +# 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.debugger.Debuggers import Debuggers diff --git a/debuginfo-tests/dexter/dex/debugger/dbgeng/README.md b/debuginfo-tests/dexter/dex/debugger/dbgeng/README.md new file mode 100644 index 00000000000..f9b864206d3 --- /dev/null +++ b/debuginfo-tests/dexter/dex/debugger/dbgeng/README.md @@ -0,0 +1,60 @@ +# Debugger Engine backend + +This directory contains the Dexter backend for the Windows Debugger Engine +(DbgEng), which powers tools such as WinDbg and CDB. + +## Overview + +DbgEng is available as a collection of unregistered COM-"like" objects that +one accesses by calling DebugCreate in DbgEng.dll. The unregistered nature +means normal COM tooling can't access them; as a result, this backend uses +ctypes to describe the COM objects and call their methods. + +This is obviously not a huge amount of fun; on the other hand, COM has +maintained ABI compatible interfaces for decades, and nothing is for free. + +The dexter backend follows the same formula as others; it creates a process +and breaks on "main", then steps through the program, observing states and +stack frames along the way. + +## Implementation details + +This backend uses a mixture of both APIs for accessing information, and the +direct command-string interface to DbgEng for performing some actions. We +have to use the DbgEng stepping interface, or we would effectively be +building a new debugger, but certain things (like enabling source-line +stepping) only seem to be possible from the command interface. + +Each segment of debugger responsibility has its own COM object: Client, +Control, Symbols, SymbolGroups, Breakpoint, SystemObjects. In this python +wrapper, each COM object gets a python object wrapping it. COM methods +that are relevant to our interests have a python method that wraps the COM +one and performs data marshalling. Some additional helper methods are added +to the python objects to extract data. + +The majority of the work occurs in setup.py and probe_process.py. The +former contains routines to launch a process and attach the debugger to +it, while the latter extracts as much information as possible from a +stopped process, returning a list of stack frames with associated variable +information. + +## Sharp edges + +For reasons still unclear, using CreateProcessAndAttach never appears to +allow the debuggee to resume, hence this implementation creates the +debuggee process manually, attaches, and resumes. + +On process startup, we set a breakpoint on main and then continue running +to it. This has the potential to never complete -- although of course, +there's no guarantee that the debuggee will ever do anything anyway. + +There doesn't appear to be a way to instruct DbgEng to "step into" a +function call, thus after reaching main, we scan the module for all +functions with line numbers in the source directory, and put breakpoints +on them. An alternative implementation would be putting breakpoints on +every known line number. + +Finally, it's unclear whether arbitrary expressions can be evaluated in +arbitrary stack frames, although this isn't something that Dexter currently +supports. + diff --git a/debuginfo-tests/dexter/dex/debugger/dbgeng/__init__.py b/debuginfo-tests/dexter/dex/debugger/dbgeng/__init__.py new file mode 100644 index 00000000000..3c458f955b7 --- /dev/null +++ b/debuginfo-tests/dexter/dex/debugger/dbgeng/__init__.py @@ -0,0 +1,19 @@ +# 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 . import dbgeng + +import platform +if platform.system() == 'Windows': + from . import breakpoint + from . import control + from . import probe_process + from . import setup + from . import symbols + from . import symgroup + from . import sysobjs + from . import utils diff --git a/debuginfo-tests/dexter/dex/debugger/dbgeng/breakpoint.py b/debuginfo-tests/dexter/dex/debugger/dbgeng/breakpoint.py new file mode 100644 index 00000000000..c966d8c9c88 --- /dev/null +++ b/debuginfo-tests/dexter/dex/debugger/dbgeng/breakpoint.py @@ -0,0 +1,88 @@ +# 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 ctypes import * +from enum import * +from functools import partial + +from .utils import * + +class BreakpointTypes(IntEnum): + DEBUG_BREAKPOINT_CODE = 0 + DEBUG_BREAKPOINT_DATA = 1 + DEBUG_BREAKPOINT_TIME = 2 + DEBUG_BREAKPOINT_INLINE = 3 + +class BreakpointFlags(IntFlag): + DEBUG_BREAKPOINT_GO_ONLY = 0x00000001 + DEBUG_BREAKPOINT_DEFERRED = 0x00000002 + DEBUG_BREAKPOINT_ENABLED = 0x00000004 + DEBUG_BREAKPOINT_ADDER_ONLY = 0x00000008 + DEBUG_BREAKPOINT_ONE_SHOT = 0x00000010 + +DebugBreakpoint2IID = IID(0x1b278d20, 0x79f2, 0x426e, IID_Data4_Type(0xa3, 0xf9, 0xc1, 0xdd, 0xf3, 0x75, 0xd4, 0x8e)) + +class DebugBreakpoint2(Structure): + pass + +class DebugBreakpoint2Vtbl(Structure): + wrp = partial(WINFUNCTYPE, c_long, POINTER(DebugBreakpoint2)) + idb_setoffset = wrp(c_ulonglong) + idb_setflags = wrp(c_ulong) + _fields_ = [ + ("QueryInterface", c_void_p), + ("AddRef", c_void_p), + ("Release", c_void_p), + ("GetId", c_void_p), + ("GetType", c_void_p), + ("GetAdder", c_void_p), + ("GetFlags", c_void_p), + ("AddFlags", c_void_p), + ("RemoveFlags", c_void_p), + ("SetFlags", idb_setflags), + ("GetOffset", c_void_p), + ("SetOffset", idb_setoffset), + ("GetDataParameters", c_void_p), + ("SetDataParameters", c_void_p), + ("GetPassCount", c_void_p), + ("SetPassCount", c_void_p), + ("GetCurrentPassCount", c_void_p), + ("GetMatchThreadId", c_void_p), + ("SetMatchThreadId", c_void_p), + ("GetCommand", c_void_p), + ("SetCommand", c_void_p), + ("GetOffsetExpression", c_void_p), + ("SetOffsetExpression", c_void_p), + ("GetParameters", c_void_p), + ("GetCommandWide", c_void_p), + ("SetCommandWide", c_void_p), + ("GetOffsetExpressionWide", c_void_p), + ("SetOffsetExpressionWide", c_void_p) + ] + +DebugBreakpoint2._fields_ = [("lpVtbl", POINTER(DebugBreakpoint2Vtbl))] + +class Breakpoint(object): + def __init__(self, breakpoint): + self.breakpoint = breakpoint.contents + self.vt = self.breakpoint.lpVtbl.contents + + def SetFlags(self, flags): + res = self.vt.SetFlags(self.breakpoint, flags) + aborter(res, "Breakpoint SetFlags") + + def SetOffset(self, offs): + res = self.vt.SetOffset(self.breakpoint, offs) + aborter(res, "Breakpoint SetOffset") + + def RemoveFlags(self, flags): + res = self.vt.RemoveFlags(self.breakpoint, flags) + aborter(res, "Breakpoint RemoveFlags") + + def die(self): + self.breakpoint = None + self.vt = None diff --git a/debuginfo-tests/dexter/dex/debugger/dbgeng/client.py b/debuginfo-tests/dexter/dex/debugger/dbgeng/client.py new file mode 100644 index 00000000000..a65e4ded2f3 --- /dev/null +++ b/debuginfo-tests/dexter/dex/debugger/dbgeng/client.py @@ -0,0 +1,185 @@ +# 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 ctypes import * +from enum import * +from functools import partial + +from .utils import * +from . import control +from . import symbols +from . import sysobjs + +class DebugAttach(IntFlag): + DEBUG_ATTACH_DEFAULT = 0 + DEBUG_ATTACH_NONINVASIVE = 1 + DEBUG_ATTACH_EXISTING = 2 + DEBUG_ATTACH_NONINVASIVE_NO_SUSPEND = 4 + DEBUG_ATTACH_INVASIVE_NO_INITIAL_BREAK = 8 + DEBUG_ATTACH_INVASIVE_RESUME_PROCESS = 0x10 + DEBUG_ATTACH_NONINVASIVE_ALLOW_PARTIAL = 0x20 + +# UUID for DebugClient7 interface. +DebugClient7IID = IID(0x13586be3, 0x542e, 0x481e, IID_Data4_Type(0xb1, 0xf2, 0x84, 0x97, 0xba, 0x74, 0xf9, 0xa9 )) + +class IDebugClient7(Structure): + pass + +class IDebugClient7Vtbl(Structure): + wrp = partial(WINFUNCTYPE, c_long, POINTER(IDebugClient7)) + idc_queryinterface = wrp(POINTER(IID), POINTER(c_void_p)) + idc_attachprocess = wrp(c_longlong, c_long, c_long) + idc_detachprocesses = wrp() + _fields_ = [ + ("QueryInterface", idc_queryinterface), + ("AddRef", c_void_p), + ("Release", c_void_p), + ("AttachKernel", c_void_p), + ("GetKernelConnectionOptions", c_void_p), + ("SetKernelConnectionOptions", c_void_p), + ("StartProcessServer", c_void_p), + ("ConnectProcessServer", c_void_p), + ("DisconnectProcessServer", c_void_p), + ("GetRunningProcessSystemIds", c_void_p), + ("GetRunningProcessSystemIdsByExecutableName", c_void_p), + ("GetRunningProcessDescription", c_void_p), + ("AttachProcess", idc_attachprocess), + ("CreateProcess", c_void_p), + ("CreateProcessAndAttach", c_void_p), + ("GetProcessOptions", c_void_p), + ("AddProcessOptions", c_void_p), + ("RemoveProcessOptions", c_void_p), + ("SetProcessOptions", c_void_p), + ("OpenDumpFile", c_void_p), + ("WriteDumpFile", c_void_p), + ("ConnectSession", c_void_p), + ("StartServer", c_void_p), + ("OutputServers", c_void_p), + ("TerminateProcesses", c_void_p), + ("DetachProcesses", idc_detachprocesses), + ("EndSession", c_void_p), + ("GetExitCode", c_void_p), + ("DispatchCallbacks", c_void_p), + ("ExitDispatch", c_void_p), + ("CreateClient", c_void_p), + ("GetInputCallbacks", c_void_p), + ("SetInputCallbacks", c_void_p), + ("GetOutputCallbacks", c_void_p), + ("SetOutputCallbacks", c_void_p), + ("GetOutputMask", c_void_p), + ("SetOutputMask", c_void_p), + ("GetOtherOutputMask", c_void_p), + ("SetOtherOutputMask", c_void_p), + ("GetOutputWidth", c_void_p), + ("SetOutputWidth", c_void_p), + ("GetOutputLinePrefix", c_void_p), + ("SetOutputLinePrefix", c_void_p), + ("GetIdentity", c_void_p), + ("OutputIdentity", c_void_p), + ("GetEventCallbacks", c_void_p), + ("SetEventCallbacks", c_void_p), + ("FlushCallbacks", c_void_p), + ("WriteDumpFile2", c_void_p), + ("AddDumpInformationFile", c_void_p), + ("EndProcessServer", c_void_p), + ("WaitForProcessServerEnd", c_void_p), + ("IsKernelDebuggerEnabled", c_void_p), + ("TerminateCurrentProcess", c_void_p), + ("DetachCurrentProcess", c_void_p), + ("AbandonCurrentProcess", c_void_p), + ("GetRunningProcessSystemIdByExecutableNameWide", c_void_p), + ("GetRunningProcessDescriptionWide", c_void_p), + ("CreateProcessWide", c_void_p), + ("CreateProcessAndAttachWide", c_void_p), + ("OpenDumpFileWide", c_void_p), + ("WriteDumpFileWide", c_void_p), + ("AddDumpInformationFileWide", c_void_p), + ("GetNumberDumpFiles", c_void_p), + ("GetDumpFile", c_void_p), + ("GetDumpFileWide", c_void_p), + ("AttachKernelWide", c_void_p), + ("GetKernelConnectionOptionsWide", c_void_p), + ("SetKernelConnectionOptionsWide", c_void_p), + ("StartProcessServerWide", c_void_p), + ("ConnectProcessServerWide", c_void_p), + ("StartServerWide", c_void_p), + ("OutputServerWide", c_void_p), + ("GetOutputCallbacksWide", c_void_p), + ("SetOutputCallbacksWide", c_void_p), + ("GetOutputLinePrefixWide", c_void_p), + ("SetOutputLinePrefixWide", c_void_p), + ("GetIdentityWide", c_void_p), + ("OutputIdentityWide", c_void_p), + ("GetEventCallbacksWide", c_void_p), + ("SetEventCallbacksWide", c_void_p), + ("CreateProcess2", c_void_p), + ("CreateProcess2Wide", c_void_p), + ("CreateProcessAndAttach2", c_void_p), + ("CreateProcessAndAttach2Wide", c_void_p), + ("PushOutputLinePrefix", c_void_p), + ("PushOutputLinePrefixWide", c_void_p), + ("PopOutputLinePrefix", c_void_p), + ("GetNumberInputCallbacks", c_void_p), + ("GetNumberOutputCallbacks", c_void_p), + ("GetNumberEventCallbacks", c_void_p), + ("GetQuitLockString", c_void_p), + ("SetQuitLockString", c_void_p), + ("GetQuitLockStringWide", c_void_p), + ("SetQuitLockStringWide", c_void_p), + ("SetEventContextCallbacks", c_void_p), + ("SetClientContext", c_void_p), + ] + +IDebugClient7._fields_ = [("lpVtbl", POINTER(IDebugClient7Vtbl))] + +class Client(object): + def __init__(self): + DbgEng = WinDLL("DbgEng") + DbgEng.DebugCreate.argtypes = [POINTER(IID), POINTER(POINTER(IDebugClient7))] + DbgEng.DebugCreate.restype = c_ulong + + # Call DebugCreate to create a new debug client + ptr = POINTER(IDebugClient7)() + res = DbgEng.DebugCreate(byref(DebugClient7IID), ptr) + aborter(res, "DebugCreate") + self.client = ptr.contents + self.vt = vt = self.client.lpVtbl.contents + + def QI(iface, ptr): + return vt.QueryInterface(self.client, byref(iface), byref(ptr)) + + # Query for a control object + ptr = c_void_p() + res = QI(control.DebugControl7IID, ptr) + aborter(res, "QueryInterface control") + self.control_ptr = cast(ptr, POINTER(control.IDebugControl7)) + self.Control = control.Control(self.control_ptr) + + # Query for a SystemObjects object + ptr = c_void_p() + res = QI(sysobjs.DebugSystemObjects4IID, ptr) + aborter(res, "QueryInterface sysobjects") + self.sysobjects_ptr = cast(ptr, POINTER(sysobjs.IDebugSystemObjects4)) + self.SysObjects = sysobjs.SysObjects(self.sysobjects_ptr) + + # Query for a Symbols object + ptr = c_void_p() + res = QI(symbols.DebugSymbols5IID, ptr) + aborter(res, "QueryInterface debugsymbosl5") + self.symbols_ptr = cast(ptr, POINTER(symbols.IDebugSymbols5)) + self.Symbols = symbols.Symbols(self.symbols_ptr) + + def AttachProcess(self, pid): + # Zero process-server id means no process-server. + res = self.vt.AttachProcess(self.client, 0, pid, DebugAttach.DEBUG_ATTACH_DEFAULT) + aborter(res, "AttachProcess") + return + + def DetachProcesses(self): + res = self.vt.DetachProcesses(self.client) + aborter(res, "DetachProcesses") + return diff --git a/debuginfo-tests/dexter/dex/debugger/dbgeng/control.py b/debuginfo-tests/dexter/dex/debugger/dbgeng/control.py new file mode 100644 index 00000000000..38585c83f70 --- /dev/null +++ b/debuginfo-tests/dexter/dex/debugger/dbgeng/control.py @@ -0,0 +1,405 @@ +# 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 ctypes import * +from functools import partial + +from .utils import * +from .breakpoint import * + +class DEBUG_STACK_FRAME_EX(Structure): + _fields_ = [ + ("InstructionOffset", c_ulonglong), + ("ReturnOffset", c_ulonglong), + ("FrameOffset", c_ulonglong), + ("StackOffset", c_ulonglong), + ("FuncTableEntry", c_ulonglong), + ("Params", c_ulonglong * 4), + ("Reserved", c_ulonglong * 6), + ("Virtual", c_bool), + ("FrameNumber", c_ulong), + ("InlineFrameContext", c_ulong), + ("Reserved1", c_ulong) + ] +PDEBUG_STACK_FRAME_EX = POINTER(DEBUG_STACK_FRAME_EX) + +class DEBUG_VALUE_U(Union): + _fields_ = [ + ("I8", c_byte), + ("I16", c_short), + ("I32", c_int), + ("I64", c_long), + ("F32", c_float), + ("F64", c_double), + ("RawBytes", c_ubyte * 24) # Force length to 24b. + ] + +class DEBUG_VALUE(Structure): + _fields_ = [ + ("U", DEBUG_VALUE_U), + ("TailOfRawBytes", c_ulong), + ("Type", c_ulong) + ] +PDEBUG_VALUE = POINTER(DEBUG_VALUE) + +class DebugValueType(IntEnum): + DEBUG_VALUE_INVALID = 0 + DEBUG_VALUE_INT8 = 1 + DEBUG_VALUE_INT16 = 2 + DEBUG_VALUE_INT32 = 3 + DEBUG_VALUE_INT64 = 4 + DEBUG_VALUE_FLOAT32 = 5 + DEBUG_VALUE_FLOAT64 = 6 + DEBUG_VALUE_FLOAT80 = 7 + DEBUG_VALUE_FLOAT82 = 8 + DEBUG_VALUE_FLOAT128 = 9 + DEBUG_VALUE_VECTOR64 = 10 + DEBUG_VALUE_VECTOR128 = 11 + DEBUG_VALUE_TYPES = 12 + +# UUID for DebugControl7 interface. +DebugControl7IID = IID(0xb86fb3b1, 0x80d4, 0x475b, IID_Data4_Type(0xae, 0xa3, 0xcf, 0x06, 0x53, 0x9c, 0xf6, 0x3a)) + +class IDebugControl7(Structure): + pass + +class IDebugControl7Vtbl(Structure): + wrp = partial(WINFUNCTYPE, c_long, POINTER(IDebugControl7)) + idc_getnumbereventfilters = wrp(c_ulong_p, c_ulong_p, c_ulong_p) + idc_setexceptionfiltersecondcommand = wrp(c_ulong, c_char_p) + idc_waitforevent = wrp(c_long, c_long) + idc_execute = wrp(c_long, c_char_p, c_long) + idc_setexpressionsyntax = wrp(c_ulong) + idc_addbreakpoint2 = wrp(c_ulong, c_ulong, POINTER(POINTER(DebugBreakpoint2))) + idc_setexecutionstatus = wrp(c_ulong) + idc_getexecutionstatus = wrp(c_ulong_p) + idc_getstacktraceex = wrp(c_ulonglong, c_ulonglong, c_ulonglong, PDEBUG_STACK_FRAME_EX, c_ulong, c_ulong_p) + idc_evaluate = wrp(c_char_p, c_ulong, PDEBUG_VALUE, c_ulong_p) + _fields_ = [ + ("QueryInterface", c_void_p), + ("AddRef", c_void_p), + ("Release", c_void_p), + ("GetInterrupt", c_void_p), + ("SetInterrupt", c_void_p), + ("GetInterruptTimeout", c_void_p), + ("SetInterruptTimeout", c_void_p), + ("GetLogFile", c_void_p), + ("OpenLogFile", c_void_p), + ("CloseLogFile", c_void_p), + ("GetLogMask", c_void_p), + ("SetLogMask", c_void_p), + ("Input", c_void_p), + ("ReturnInput", c_void_p), + ("Output", c_void_p), + ("OutputVaList", c_void_p), + ("ControlledOutput", c_void_p), + ("ControlledOutputVaList", c_void_p), + ("OutputPrompt", c_void_p), + ("OutputPromptVaList", c_void_p), + ("GetPromptText", c_void_p), + ("OutputCurrentState", c_void_p), + ("OutputVersionInformation", c_void_p), + ("GetNotifyEventHandle", c_void_p), + ("SetNotifyEventHandle", c_void_p), + ("Assemble", c_void_p), + ("Disassemble", c_void_p), + ("GetDisassembleEffectiveOffset", c_void_p), + ("OutputDisassembly", c_void_p), + ("OutputDisassemblyLines", c_void_p), + ("GetNearInstruction", c_void_p), + ("GetStackTrace", c_void_p), + ("GetReturnOffset", c_void_p), + ("OutputStackTrace", c_void_p), + ("GetDebuggeeType", c_void_p), + ("GetActualProcessorType", c_void_p), + ("GetExecutingProcessorType", c_void_p), + ("GetNumberPossibleExecutingProcessorTypes", c_void_p), + ("GetPossibleExecutingProcessorTypes", c_void_p), + ("GetNumberProcessors", c_void_p), + ("GetSystemVersion", c_void_p), + ("GetPageSize", c_void_p), + ("IsPointer64Bit", c_void_p), + ("ReadBugCheckData", c_void_p), + ("GetNumberSupportedProcessorTypes", c_void_p), + ("GetSupportedProcessorTypes", c_void_p), + ("GetProcessorTypeNames", c_void_p), + ("GetEffectiveProcessorType", c_void_p), + ("SetEffectiveProcessorType", c_void_p), + ("GetExecutionStatus", idc_getexecutionstatus), + ("SetExecutionStatus", idc_setexecutionstatus), + ("GetCodeLevel", c_void_p), + ("SetCodeLevel", c_void_p), + ("GetEngineOptions", c_void_p), + ("AddEngineOptions", c_void_p), + ("RemoveEngineOptions", c_void_p), + ("SetEngineOptions", c_void_p), + ("GetSystemErrorControl", c_void_p), + ("SetSystemErrorControl", c_void_p), + ("GetTextMacro", c_void_p), + ("SetTextMacro", c_void_p), + ("GetRadix", c_void_p), + ("SetRadix", c_void_p), + ("Evaluate", idc_evaluate), + ("CoerceValue", c_void_p), + ("CoerceValues", c_void_p), + ("Execute", idc_execute), + ("ExecuteCommandFile", c_void_p), + ("GetNumberBreakpoints", c_void_p), + ("GetBreakpointByIndex", c_void_p), + ("GetBreakpointById", c_void_p), + ("GetBreakpointParameters", c_void_p), + ("AddBreakpoint", c_void_p), + ("RemoveBreakpoint", c_void_p), + ("AddExtension", c_void_p), + ("RemoveExtension", c_void_p), + ("GetExtensionByPath", c_void_p), + ("CallExtension", c_void_p), + ("GetExtensionFunction", c_void_p), + ("GetWindbgExtensionApis32", c_void_p), + ("GetWindbgExtensionApis64", c_void_p), + ("GetNumberEventFilters", idc_getnumbereventfilters), + ("GetEventFilterText", c_void_p), + ("GetEventFilterCommand", c_void_p), + ("SetEventFilterCommand", c_void_p), + ("GetSpecificFilterParameters", c_void_p), + ("SetSpecificFilterParameters", c_void_p), + ("GetSpecificFilterArgument", c_void_p), + ("SetSpecificFilterArgument", c_void_p), + ("GetExceptionFilterParameters", c_void_p), + ("SetExceptionFilterParameters", c_void_p), + ("GetExceptionFilterSecondCommand", c_void_p), + ("SetExceptionFilterSecondCommand", idc_setexceptionfiltersecondcommand), + ("WaitForEvent", idc_waitforevent), + ("GetLastEventInformation", c_void_p), + ("GetCurrentTimeDate", c_void_p), + ("GetCurrentSystemUpTime", c_void_p), + ("GetDumpFormatFlags", c_void_p), + ("GetNumberTextReplacements", c_void_p), + ("GetTextReplacement", c_void_p), + ("SetTextReplacement", c_void_p), + ("RemoveTextReplacements", c_void_p), + ("OutputTextReplacements", c_void_p), + ("GetAssemblyOptions", c_void_p), + ("AddAssemblyOptions", c_void_p), + ("RemoveAssemblyOptions", c_void_p), + ("SetAssemblyOptions", c_void_p), + ("GetExpressionSyntax", c_void_p), + ("SetExpressionSyntax", idc_setexpressionsyntax), + ("SetExpressionSyntaxByName", c_void_p), + ("GetNumberExpressionSyntaxes", c_void_p), + ("GetExpressionSyntaxNames", c_void_p), + ("GetNumberEvents", c_void_p), + ("GetEventIndexDescription", c_void_p), + ("GetCurrentEventIndex", c_void_p), + ("SetNextEventIndex", c_void_p), + ("GetLogFileWide", c_void_p), + ("OpenLogFileWide", c_void_p), + ("InputWide", c_void_p), + ("ReturnInputWide", c_void_p), + ("OutputWide", c_void_p), + ("OutputVaListWide", c_void_p), + ("ControlledOutputWide", c_void_p), + ("ControlledOutputVaListWide", c_void_p), + ("OutputPromptWide", c_void_p), + ("OutputPromptVaListWide", c_void_p), + ("GetPromptTextWide", c_void_p), + ("AssembleWide", c_void_p), + ("DisassembleWide", c_void_p), + ("GetProcessrTypeNamesWide", c_void_p), + ("GetTextMacroWide", c_void_p), + ("SetTextMacroWide", c_void_p), + ("EvaluateWide", c_void_p), + ("ExecuteWide", c_void_p), + ("ExecuteCommandFileWide", c_void_p), + ("GetBreakpointByIndex2", c_void_p), + ("GetBreakpointById2", c_void_p), + ("AddBreakpoint2", idc_addbreakpoint2), + ("RemoveBreakpoint2", c_void_p), + ("AddExtensionWide", c_void_p), + ("GetExtensionByPathWide", c_void_p), + ("CallExtensionWide", c_void_p), + ("GetExtensionFunctionWide", c_void_p), + ("GetEventFilterTextWide", c_void_p), + ("GetEventfilterCommandWide", c_void_p), + ("SetEventFilterCommandWide", c_void_p), + ("GetSpecificFilterArgumentWide", c_void_p), + ("SetSpecificFilterArgumentWide", c_void_p), + ("GetExceptionFilterSecondCommandWide", c_void_p), + ("SetExceptionFilterSecondCommandWider", c_void_p), + ("GetLastEventInformationWide", c_void_p), + ("GetTextReplacementWide", c_void_p), + ("SetTextReplacementWide", c_void_p), + ("SetExpressionSyntaxByNameWide", c_void_p), + ("GetExpressionSyntaxNamesWide", c_void_p), + ("GetEventIndexDescriptionWide", c_void_p), + ("GetLogFile2", c_void_p), + ("OpenLogFile2", c_void_p), + ("GetLogFile2Wide", c_void_p), + ("OpenLogFile2Wide", c_void_p), + ("GetSystemVersionValues", c_void_p), + ("GetSystemVersionString", c_void_p), + ("GetSystemVersionStringWide", c_void_p), + ("GetContextStackTrace", c_void_p), + ("OutputContextStackTrace", c_void_p), + ("GetStoredEventInformation", c_void_p), + ("GetManagedStatus", c_void_p), + ("GetManagedStatusWide", c_void_p), + ("ResetManagedStatus", c_void_p), + ("GetStackTraceEx", idc_getstacktraceex), + ("OutputStackTraceEx", c_void_p), + ("GetContextStackTraceEx", c_void_p), + ("OutputContextStackTraceEx", c_void_p), + ("GetBreakpointByGuid", c_void_p), + ("GetExecutionStatusEx", c_void_p), + ("GetSynchronizationStatus", c_void_p), + ("GetDebuggeeType2", c_void_p) + ] + +IDebugControl7._fields_ = [("lpVtbl", POINTER(IDebugControl7Vtbl))] + +class DebugStatus(IntEnum): + DEBUG_STATUS_NO_CHANGE = 0 + DEBUG_STATUS_GO = 1 + DEBUG_STATUS_GO_HANDLED = 2 + DEBUG_STATUS_GO_NOT_HANDLED = 3 + DEBUG_STATUS_STEP_OVER = 4 + DEBUG_STATUS_STEP_INTO = 5 + DEBUG_STATUS_BREAK = 6 + DEBUG_STATUS_NO_DEBUGGEE = 7 + DEBUG_STATUS_STEP_BRANCH = 8 + DEBUG_STATUS_IGNORE_EVENT = 9 + DEBUG_STATUS_RESTART_REQUESTED = 10 + DEBUG_STATUS_REVERSE_GO = 11 + DEBUG_STATUS_REVERSE_STEP_BRANCH = 12 + DEBUG_STATUS_REVERSE_STEP_OVER = 13 + DEBUG_STATUS_REVERSE_STEP_INTO = 14 + DEBUG_STATUS_OUT_OF_SYNC = 15 + DEBUG_STATUS_WAIT_INPUT = 16 + DEBUG_STATUS_TIMEOUT = 17 + +class DebugSyntax(IntEnum): + DEBUG_EXPR_MASM = 0 + DEBUG_EXPR_CPLUSPLUS = 1 + +class Control(object): + def __init__(self, control): + self.ptr = control + self.control = control.contents + self.vt = self.control.lpVtbl.contents + # Keep a handy ulong for passing into C methods. + self.ulong = c_ulong() + + def GetExecutionStatus(self, doprint=False): + ret = self.vt.GetExecutionStatus(self.control, byref(self.ulong)) + aborter(ret, "GetExecutionStatus") + status = DebugStatus(self.ulong.value) + if doprint: + print("Execution status: {}".format(status)) + return status + + def SetExecutionStatus(self, status): + assert isinstance(status, DebugStatus) + res = self.vt.SetExecutionStatus(self.control, status.value) + aborter(res, "SetExecutionStatus") + + def WaitForEvent(self, timeout=100): + # No flags are taken by WaitForEvent, hence 0 + ret = self.vt.WaitForEvent(self.control, 0, timeout) + aborter(ret, "WaitforEvent", ignore=[S_FALSE]) + return ret + + def GetNumberEventFilters(self): + specific_events = c_ulong() + specific_exceptions = c_ulong() + arbitrary_exceptions = c_ulong() + res = self.vt.GetNumberEventFilters(self.control, byref(specific_events), + byref(specific_exceptions), + byref(arbitrary_exceptions)) + aborter(res, "GetNumberEventFilters") + return (specific_events.value, specific_exceptions.value, + arbitrary_exceptions.value) + + def SetExceptionFilterSecondCommand(self, index, command): + buf = create_string_buffer(command.encode('ascii')) + res = self.vt.SetExceptionFilterSecondCommand(self.control, index, buf) + aborter(res, "SetExceptionFilterSecondCommand") + return + + def AddBreakpoint2(self, offset=None, enabled=None): + breakpoint = POINTER(DebugBreakpoint2)() + res = self.vt.AddBreakpoint2(self.control, BreakpointTypes.DEBUG_BREAKPOINT_CODE, DEBUG_ANY_ID, byref(breakpoint)) + aborter(res, "Add breakpoint 2") + bp = Breakpoint(breakpoint) + + if offset is not None: + bp.SetOffset(offset) + if enabled is not None and enabled: + bp.SetFlags(BreakpointFlags.DEBUG_BREAKPOINT_ENABLED) + + return bp + + def RemoveBreakpoint(self, bp): + res = self.vt.RemoveBreakpoint2(self.control, bp.breakpoint) + aborter(res, "RemoveBreakpoint2") + bp.die() + + def GetStackTraceEx(self): + # XXX -- I can't find a way to query for how many stack frames there _are_ + # in advance. Guess 128 for now. + num_frames_buffer = 128 + + frames = (DEBUG_STACK_FRAME_EX * num_frames_buffer)() + numframes = c_ulong() + + # First three args are frame/stack/IP offsets -- leave them as zero to + # default to the current instruction. + res = self.vt.GetStackTraceEx(self.control, 0, 0, 0, frames, num_frames_buffer, byref(numframes)) + aborter(res, "GetStackTraceEx") + return frames, numframes.value + + def Execute(self, command): + # First zero is DEBUG_OUTCTL_*, which we leave as a default, second + # zero is DEBUG_EXECUTE_* flags, of which we set none. + res = self.vt.Execute(self.control, 0, command.encode('ascii'), 0) + aborter(res, "Client execute") + + def SetExpressionSyntax(self, cpp=True): + if cpp: + syntax = DebugSyntax.DEBUG_EXPR_CPLUSPLUS + else: + syntax = DebugSyntax.DEBUG_EXPR_MASM + + res = self.vt.SetExpressionSyntax(self.control, syntax) + aborter(res, "SetExpressionSyntax") + + def Evaluate(self, expr): + ptr = DEBUG_VALUE() + res = self.vt.Evaluate(self.control, expr.encode("ascii"), DebugValueType.DEBUG_VALUE_INVALID, byref(ptr), None) + aborter(res, "Evaluate", ignore=[E_INTERNALEXCEPTION, E_FAIL]) + if res != 0: + return None + + val_type = DebugValueType(ptr.Type) + + # Here's a map from debug value types to fields. Unclear what happens + # with unsigned values, as DbgEng doesn't present any unsigned fields. + + extract_map = { + DebugValueType.DEBUG_VALUE_INT8 : ("I8", "char"), + DebugValueType.DEBUG_VALUE_INT16 : ("I16", "short"), + DebugValueType.DEBUG_VALUE_INT32 : ("I32", "int"), + DebugValueType.DEBUG_VALUE_INT64 : ("I64", "long"), + DebugValueType.DEBUG_VALUE_FLOAT32 : ("F32", "float"), + DebugValueType.DEBUG_VALUE_FLOAT64 : ("F64", "double") + } # And everything else is invalid. + + if val_type not in extract_map: + raise Exception("Unexpected debug value type {} when evalutaing".format(val_type)) + + # Also produce a type name... + + return getattr(ptr.U, extract_map[val_type][0]), extract_map[val_type][1] diff --git a/debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py b/debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py new file mode 100644 index 00000000000..66d01f03e8f --- /dev/null +++ b/debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py @@ -0,0 +1,163 @@ +# 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 + +import sys +import os +import platform + +from dex.debugger.DebuggerBase import DebuggerBase +from dex.dextIR import FrameIR, LocIR, StepIR, StopReason, ValueIR +from dex.dextIR import ProgramState, StackFrame, SourceLocation +from dex.utils.Exceptions import DebuggerException, LoadDebuggerException +from dex.utils.ReturnCode import ReturnCode + +if platform.system() == "Windows": + # Don't load on linux; _load_interface will croak before any names are used. + from . import setup + from . import probe_process + from . import breakpoint + +class DbgEng(DebuggerBase): + def __init__(self, context, *args): + self.breakpoints = [] + self.running = False + self.finished = False + self.step_info = None + super(DbgEng, self).__init__(context, *args) + + def _custom_init(self): + try: + res = setup.setup_everything(self.context.options.executable) + self.client, self.hProcess = res + self.running = True + except Exception as e: + raise Exception('Failed to start debuggee: {}'.format(e)) + + def _custom_exit(self): + setup.cleanup(self.client, self.hProcess) + + def _load_interface(self): + arch = platform.architecture()[0] + machine = platform.machine() + if arch == '32bit' and machine == 'AMD64': + # This python process is 32 bits, but is sitting on a 64 bit machine. + # Bad things may happen, don't support it. + raise LoadDebuggerException('Can\'t run Dexter dbgeng on 32 bit python in a 64 bit environment') + + if platform.system() != 'Windows': + raise LoadDebuggerException('DbgEng supports Windows only') + + # Otherwise, everything was imported earlier + + @classmethod + def get_name(cls): + return 'dbgeng' + + @classmethod + def get_option_name(cls): + return 'dbgeng' + + @property + def frames_below_main(self): + return [] + + @property + def version(self): + # I don't believe there's a well defined DbgEng version, outside of the + # version of Windows being used. + return "1" + + def clear_breakpoints(self): + for x in self.breakpoints: + x.RemoveFlags(breakpoint.BreakpointFlags.DEBUG_BREAKPOINT_ENABLED) + self.client.Control.RemoveBreakpoint(x) + + def add_breakpoint(self, file_, line): + # This is something to implement in the future -- as it stands, Dexter + # doesn't test for such things as "I can set a breakpoint on this line". + # This is only called AFAICT right now to ensure we break on every step. + pass + + def launch(self): + # We are, by this point, already launched. + self.step_info = probe_process.probe_state(self.client) + + def step(self): + res = setup.step_once(self.client) + if not res: + self.finished = True + self.step_info = res + + def go(self): + # We never go -- we always single step. + pass + + def get_step_info(self): + frames = self.step_info + state_frames = [] + + # For now assume the base function is the... function, ignoring + # inlining. + dex_frames = [] + for i, x in enumerate(frames): + # XXX Might be able to get columns out through + # GetSourceEntriesByOffset, not a priority now + loc = LocIR(path=x.source_file, lineno=x.line_no, column=0) + new_frame = FrameIR(function=x.function_name, is_inlined=False, loc=loc) + dex_frames.append(new_frame) + + state_frame = StackFrame(function=new_frame.function, + is_inlined=new_frame.is_inlined, + location=SourceLocation(path=x.source_file, + lineno=x.line_no, + column=0), + watches={}) + for expr in map( + lambda watch, idx=i: self.evaluate_expression(watch, idx), + self.watches): + state_frame.watches[expr.expression] = expr + state_frames.append(state_frame) + + return StepIR( + step_index=self.step_index, frames=dex_frames, + stop_reason=StopReason.STEP, + program_state=ProgramState(state_frames)) + + @property + def is_running(self): + return False # We're never free-running + + @property + def is_finished(self): + return self.finished + + def evaluate_expression(self, expression, frame_idx=0): + # XXX: cdb insists on using '->' to examine fields of structures, + # as it appears to reserve '.' for other purposes. + fixed_expr = expression.replace('.', '->') + + orig_scope_idx = self.client.Symbols.GetCurrentScopeFrameIndex() + self.client.Symbols.SetScopeFrameByIndex(frame_idx) + + res = self.client.Control.Evaluate(fixed_expr) + if res is not None: + result, typename = self.client.Control.Evaluate(fixed_expr) + could_eval = True + else: + result, typename = (None, None) + could_eval = False + + self.client.Symbols.SetScopeFrameByIndex(orig_scope_idx) + + return ValueIR( + expression=expression, + value=str(result), + type_name=typename, + error_string="", + could_evaluate=could_eval, + is_optimized_away=False, + is_irretrievable=not could_eval) diff --git a/debuginfo-tests/dexter/dex/debugger/dbgeng/probe_process.py b/debuginfo-tests/dexter/dex/debugger/dbgeng/probe_process.py new file mode 100644 index 00000000000..8bd7f607081 --- /dev/null +++ b/debuginfo-tests/dexter/dex/debugger/dbgeng/probe_process.py @@ -0,0 +1,80 @@ +# 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 + +import os + +from .utils import * + +class Frame(object): + def __init__(self, frame, idx, Symbols): + # Store some base information about the frame + self.ip = frame.InstructionOffset + self.scope_idx = idx + self.virtual = frame.Virtual + self.inline_frame_context = frame.InlineFrameContext + self.func_tbl_entry = frame.FuncTableEntry + + # Fetch the module/symbol we're in, with displacement. Useful for debugging. + self.descr = Symbols.GetNearNameByOffset(self.ip) + split = self.descr.split('!')[0] + self.module = split[0] + self.symbol = split[1] + + # Fetch symbol group for this scope. + prevscope = Symbols.GetCurrentScopeFrameIndex() + if Symbols.SetScopeFrameByIndex(idx): + symgroup = Symbols.GetScopeSymbolGroup2() + Symbols.SetScopeFrameByIndex(prevscope) + self.symgroup = symgroup + else: + self.symgroup = None + + # Fetch the name according to the line-table, using inlining context. + name = Symbols.GetNameByInlineContext(self.ip, self.inline_frame_context) + self.function_name = name.split('!')[-1] + + try: + tup = Symbols.GetLineByInlineContext(self.ip, self.inline_frame_context) + self.source_file, self.line_no = tup + except WinError as e: + # Fall back to trying to use a non-inlining-aware line number + # XXX - this is not inlining aware + sym = Symbols.GetLineByOffset(self.ip) + if sym is not None: + self.source_file, self.line_no = sym + else: + self.source_file = None + self.line_no = None + self.basename = None + + if self.source_file is not None: + self.basename = os.path.basename(self.source_file) + else: + self.basename = None + + + + def __str__(self): + return '{}:{}({}) {}'.format(self.basename, self.line, self.descr, self.function_name) + +def main_on_stack(Symbols, frames): + module_name = Symbols.get_exefile_module_name() + main_name = "{}!main".format(module_name) + for x in frames: + if main_name in x.descr: # Could be less hard coded... + return True + return False + +def probe_state(Client): + # Fetch the state of the program -- represented by the stack frames. + frames, numframes = Client.Control.GetStackTraceEx() + + the_frames = [Frame(frames[x], x, Client.Symbols) for x in range(numframes)] + if not main_on_stack(Client.Symbols, the_frames): + return None + + return the_frames diff --git a/debuginfo-tests/dexter/dex/debugger/dbgeng/setup.py b/debuginfo-tests/dexter/dex/debugger/dbgeng/setup.py new file mode 100644 index 00000000000..30a62f6dd42 --- /dev/null +++ b/debuginfo-tests/dexter/dex/debugger/dbgeng/setup.py @@ -0,0 +1,185 @@ +# 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 ctypes import * + +from . import client +from . import control +from . import symbols +from .probe_process import probe_state +from .utils import * + +class STARTUPINFOA(Structure): + _fields_ = [ + ('cb', c_ulong), + ('lpReserved', c_char_p), + ('lpDesktop', c_char_p), + ('lpTitle', c_char_p), + ('dwX', c_ulong), + ('dwY', c_ulong), + ('dwXSize', c_ulong), + ('dwYSize', c_ulong), + ('dwXCountChars', c_ulong), + ('dwYCountChars', c_ulong), + ('dwFillAttribute', c_ulong), + ('wShowWindow', c_ushort), + ('cbReserved2', c_ushort), + ('lpReserved2', c_char_p), + ('hStdInput', c_void_p), + ('hStdOutput', c_void_p), + ('hStdError', c_void_p) + ] + +class PROCESS_INFORMATION(Structure): + _fields_ = [ + ('hProcess', c_void_p), + ('hThread', c_void_p), + ('dwProcessId', c_ulong), + ('dwThreadId', c_ulong) + ] + +def fetch_local_function_syms(Symbols, prefix): + syms = Symbols.get_all_functions() + + def is_sym_in_src_dir(sym): + name, data = sym + symdata = Symbols.GetLineByOffset(data.Offset) + if symdata is not None: + srcfile, line = symdata + if prefix in srcfile: + return True + return False + + syms = [x for x in syms if is_sym_in_src_dir(x)] + return syms + +def break_on_all_but_main(Control, Symbols, main_offset): + mainfile, _ = Symbols.GetLineByOffset(main_offset) + prefix = '\\'.join(mainfile.split('\\')[:-1]) + + for name, rec in fetch_local_function_syms(Symbols, prefix): + if name == "main": + continue + bp = Control.AddBreakpoint2(offset=rec.Offset, enabled=True) + + # All breakpoints are currently discarded: we just sys.exit for cleanup + return + +def process_creator(binfile): + Kernel32 = WinDLL("Kernel32") + + # Another flavour of process creation + startupinfoa = STARTUPINFOA() + startupinfoa.cb = sizeof(STARTUPINFOA) + startupinfoa.lpReserved = None + startupinfoa.lpDesktop = None + startupinfoa.lpTitle = None + startupinfoa.dwX = 0 + startupinfoa.dwY = 0 + startupinfoa.dwXSize = 0 + startupinfoa.dwYSize = 0 + startupinfoa.dwXCountChars = 0 + startupinfoa.dwYCountChars = 0 + startupinfoa.dwFillAttribute = 0 + startupinfoa.dwFlags = 0 + startupinfoa.wShowWindow = 0 + startupinfoa.cbReserved2 = 0 + startupinfoa.lpReserved2 = None + startupinfoa.hStdInput = None + startupinfoa.hStdOutput = None + startupinfoa.hStdError = None + processinformation = PROCESS_INFORMATION() + + # 0x4 below specifies CREATE_SUSPENDED. + ret = Kernel32.CreateProcessA(binfile.encode("ascii"), None, None, None, False, 0x4, None, None, byref(startupinfoa), byref(processinformation)) + if ret == 0: + raise Exception('CreateProcess running {}'.format(binfile)) + + return processinformation.dwProcessId, processinformation.dwThreadId, processinformation.hProcess, processinformation.hThread + +def thread_resumer(hProcess, hThread): + Kernel32 = WinDLL("Kernel32") + + # For reasons unclear to me, other suspend-references seem to be opened on + # the opened thread. Clear them all. + while True: + ret = Kernel32.ResumeThread(hThread) + if ret <= 0: + break + if ret < 0: + Kernel32.TerminateProcess(hProcess, 1) + raise Exception("Couldn't resume process after startup") + + return + +def setup_everything(binfile): + from . import client + from . import symbols + Client = client.Client() + + created_pid, created_tid, hProcess, hThread = process_creator(binfile) + + # Load lines as well as general symbols + sym_opts = Client.Symbols.GetSymbolOptions() + sym_opts |= symbols.SymbolOptionFlags.SYMOPT_LOAD_LINES + Client.Symbols.SetSymbolOptions(sym_opts) + + Client.AttachProcess(created_pid) + + # Need to enter the debugger engine to let it attach properly + Client.Control.WaitForEvent(timeout=1) + Client.SysObjects.set_current_thread(created_pid, created_tid) + Client.Control.Execute("l+t") + Client.Control.SetExpressionSyntax(cpp=True) + + module_name = Client.Symbols.get_exefile_module_name() + offset = Client.Symbols.GetOffsetByName("{}!main".format(module_name)) + breakpoint = Client.Control.AddBreakpoint2(offset=offset, enabled=True) + thread_resumer(hProcess, hThread) + Client.Control.SetExecutionStatus(control.DebugStatus.DEBUG_STATUS_GO) + + # Problem: there is no guarantee that the client will ever reach main, + # something else exciting could happen in that time, the host system may + # be very loaded, and similar. Wait for some period, say, five seconds, and + # abort afterwards: this is a trade-off between spurious timeouts and + # completely hanging in the case of a environmental/programming error. + res = Client.Control.WaitForEvent(timeout=5000) + if res == S_FALSE: + Kernel32.TerminateProcess(hProcess, 1) + raise Exception("Debuggee did not reach main function in a timely manner") + + break_on_all_but_main(Client.Control, Client.Symbols, offset) + + # Set the default action on all exceptions to be "quit and detach". If we + # don't, dbgeng will merrily spin at the exception site forever. + filts = Client.Control.GetNumberEventFilters() + for x in range(filts[0], filts[0] + filts[1]): + Client.Control.SetExceptionFilterSecondCommand(x, "qd") + + return Client, hProcess + +def step_once(client): + client.Control.Execute("p") + try: + client.Control.WaitForEvent() + except Exception as e: + if client.Control.GetExecutionStatus() == control.DebugStatus.DEBUG_STATUS_NO_DEBUGGEE: + return None # Debuggee has gone away, likely due to an exception. + raise e + # Could assert here that we're in the "break" state + client.Control.GetExecutionStatus() + return probe_state(client) + +def main_loop(client): + res = True + while res is not None: + res = step_once(client) + +def cleanup(client, hProcess): + res = client.DetachProcesses() + Kernel32 = WinDLL("Kernel32") + Kernel32.TerminateProcess(hProcess, 1) diff --git a/debuginfo-tests/dexter/dex/debugger/dbgeng/symbols.py b/debuginfo-tests/dexter/dex/debugger/dbgeng/symbols.py new file mode 100644 index 00000000000..bc998facb4e --- /dev/null +++ b/debuginfo-tests/dexter/dex/debugger/dbgeng/symbols.py @@ -0,0 +1,499 @@ +# 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 collections import namedtuple +from ctypes import * +from enum import * +from functools import reduce, partial + +from .symgroup import SymbolGroup, IDebugSymbolGroup2 +from .utils import * + +class SymbolOptionFlags(IntFlag): + SYMOPT_CASE_INSENSITIVE = 0x00000001 + SYMOPT_UNDNAME = 0x00000002 + SYMOPT_DEFERRED_LOADS = 0x00000004 + SYMOPT_NO_CPP = 0x00000008 + SYMOPT_LOAD_LINES = 0x00000010 + SYMOPT_OMAP_FIND_NEAREST = 0x00000020 + SYMOPT_LOAD_ANYTHING = 0x00000040 + SYMOPT_IGNORE_CVREC = 0x00000080 + SYMOPT_NO_UNQUALIFIED_LOADS = 0x00000100 + SYMOPT_FAIL_CRITICAL_ERRORS = 0x00000200 + SYMOPT_EXACT_SYMBOLS = 0x00000400 + SYMOPT_ALLOW_ABSOLUTE_SYMBOLS = 0x00000800 + SYMOPT_IGNORE_NT_SYMPATH = 0x00001000 + SYMOPT_INCLUDE_32BIT_MODULES = 0x00002000 + SYMOPT_PUBLICS_ONLY = 0x00004000 + SYMOPT_NO_PUBLICS = 0x00008000 + SYMOPT_AUTO_PUBLICS = 0x00010000 + SYMOPT_NO_IMAGE_SEARCH = 0x00020000 + SYMOPT_SECURE = 0x00040000 + SYMOPT_NO_PROMPTS = 0x00080000 + SYMOPT_DEBUG = 0x80000000 + +class ScopeGroupFlags(IntFlag): + DEBUG_SCOPE_GROUP_ARGUMENTS = 0x00000001 + DEBUG_SCOPE_GROUP_LOCALS = 0x00000002 + DEBUG_SCOPE_GROUP_ALL = 0x00000003 + DEBUG_SCOPE_GROUP_BY_DATAMODEL = 0x00000004 + +class DebugModuleNames(IntEnum): + DEBUG_MODNAME_IMAGE = 0x00000000 + DEBUG_MODNAME_MODULE = 0x00000001 + DEBUG_MODNAME_LOADED_IMAGE = 0x00000002 + DEBUG_MODNAME_SYMBOL_FILE = 0x00000003 + DEBUG_MODNAME_MAPPED_IMAGE = 0x00000004 + +class DebugModuleFlags(IntFlag): + DEBUG_MODULE_LOADED = 0x00000000 + DEBUG_MODULE_UNLOADED = 0x00000001 + DEBUG_MODULE_USER_MODE = 0x00000002 + DEBUG_MODULE_EXE_MODULE = 0x00000004 + DEBUG_MODULE_EXPLICIT = 0x00000008 + DEBUG_MODULE_SECONDARY = 0x00000010 + DEBUG_MODULE_SYNTHETIC = 0x00000020 + DEBUG_MODULE_SYM_BAD_CHECKSUM = 0x00010000 + +class DEBUG_MODULE_PARAMETERS(Structure): + _fields_ = [ + ("Base", c_ulonglong), + ("Size", c_ulong), + ("TimeDateStamp", c_ulong), + ("Checksum", c_ulong), + ("Flags", c_ulong), + ("SymbolType", c_ulong), + ("ImageNameSize", c_ulong), + ("ModuleNameSize", c_ulong), + ("LoadedImageNameSize", c_ulong), + ("SymbolFileNameSize", c_ulong), + ("MappedImageNameSize", c_ulong), + ("Reserved", c_ulonglong * 2) + ] +PDEBUG_MODULE_PARAMETERS = POINTER(DEBUG_MODULE_PARAMETERS) + +class DEBUG_MODULE_AND_ID(Structure): + _fields_ = [ + ("ModuleBase", c_ulonglong), + ("Id", c_ulonglong) + ] +PDEBUG_MODULE_AND_ID = POINTER(DEBUG_MODULE_AND_ID) + +class DEBUG_SYMBOL_ENTRY(Structure): + _fields_ = [ + ("ModuleBase", c_ulonglong), + ("Offset", c_ulonglong), + ("Id", c_ulonglong), + ("Arg64", c_ulonglong), + ("Size", c_ulong), + ("Flags", c_ulong), + ("TypeId", c_ulong), + ("NameSize", c_ulong), + ("Token", c_ulong), + ("Tag", c_ulong), + ("Arg32", c_ulong), + ("Reserved", c_ulong) + ] +PDEBUG_SYMBOL_ENTRY = POINTER(DEBUG_SYMBOL_ENTRY) + +# UUID for DebugSymbols5 interface. +DebugSymbols5IID = IID(0xc65fa83e, 0x1e69, 0x475e, IID_Data4_Type(0x8e, 0x0e, 0xb5, 0xd7, 0x9e, 0x9c, 0xc1, 0x7e)) + +class IDebugSymbols5(Structure): + pass + +class IDebugSymbols5Vtbl(Structure): + wrp = partial(WINFUNCTYPE, c_long, POINTER(IDebugSymbols5)) + ids_getsymboloptions = wrp(c_ulong_p) + ids_setsymboloptions = wrp(c_ulong) + ids_getmoduleparameters = wrp(c_ulong, c_ulong64_p, c_ulong, PDEBUG_MODULE_PARAMETERS) + ids_getmodulenamestring = wrp(c_ulong, c_ulong, c_ulonglong, c_char_p, c_ulong, c_ulong_p) + ids_getoffsetbyname = wrp(c_char_p, c_ulong64_p) + ids_getlinebyoffset = wrp(c_ulonglong, c_ulong_p, c_char_p, c_ulong, c_ulong_p, c_ulong64_p) + ids_getsymbolentriesbyname = wrp(c_char_p, c_ulong, PDEBUG_MODULE_AND_ID, c_ulong, c_ulong_p) + ids_getsymbolentrystring = wrp(PDEBUG_MODULE_AND_ID, c_ulong, c_char_p, c_ulong, c_ulong_p) + ids_getsymbolentryinformation = wrp(PDEBUG_MODULE_AND_ID, PDEBUG_SYMBOL_ENTRY) + ids_getcurrentscopeframeindex = wrp(c_ulong_p) + ids_getnearnamebyoffset = wrp(c_ulonglong, c_long, c_char_p, c_ulong, c_ulong_p, c_ulong64_p) + ids_setscopeframebyindex = wrp(c_ulong) + ids_getscopesymbolgroup2 = wrp(c_ulong, POINTER(IDebugSymbolGroup2), POINTER(POINTER(IDebugSymbolGroup2))) + ids_getnamebyinlinecontext = wrp(c_ulonglong, c_ulong, c_char_p, c_ulong, c_ulong_p, c_ulong64_p) + ids_getlinebyinlinecontext = wrp(c_ulonglong, c_ulong, c_ulong_p, c_char_p, c_ulong, c_ulong_p, c_ulong64_p) + _fields_ = [ + ("QueryInterface", c_void_p), + ("AddRef", c_void_p), + ("Release", c_void_p), + ("GetSymbolOptions", ids_getsymboloptions), + ("AddSymbolOptions", c_void_p), + ("RemoveSymbolOptions", c_void_p), + ("SetSymbolOptions", ids_setsymboloptions), + ("GetNameByOffset", c_void_p), + ("GetOffsetByName", ids_getoffsetbyname), + ("GetNearNameByOffset", ids_getnearnamebyoffset), + ("GetLineByOffset", ids_getlinebyoffset), + ("GetOffsetByLine", c_void_p), + ("GetNumberModules", c_void_p), + ("GetModuleByIndex", c_void_p), + ("GetModuleByModuleName", c_void_p), + ("GetModuleByOffset", c_void_p), + ("GetModuleNames", c_void_p), + ("GetModuleParameters", ids_getmoduleparameters), + ("GetSymbolModule", c_void_p), + ("GetTypeName", c_void_p), + ("GetTypeId", c_void_p), + ("GetTypeSize", c_void_p), + ("GetFieldOffset", c_void_p), + ("GetSymbolTypeId", c_void_p), + ("GetOffsetTypeId", c_void_p), + ("ReadTypedDataVirtual", c_void_p), + ("WriteTypedDataVirtual", c_void_p), + ("OutputTypedDataVirtual", c_void_p), + ("ReadTypedDataPhysical", c_void_p), + ("WriteTypedDataPhysical", c_void_p), + ("OutputTypedDataPhysical", c_void_p), + ("GetScope", c_void_p), + ("SetScope", c_void_p), + ("ResetScope", c_void_p), + ("GetScopeSymbolGroup", c_void_p), + ("CreateSymbolGroup", c_void_p), + ("StartSymbolMatch", c_void_p), + ("GetNextSymbolMatch", c_void_p), + ("EndSymbolMatch", c_void_p), + ("Reload", c_void_p), + ("GetSymbolPath", c_void_p), + ("SetSymbolPath", c_void_p), + ("AppendSymbolPath", c_void_p), + ("GetImagePath", c_void_p), + ("SetImagePath", c_void_p), + ("AppendImagePath", c_void_p), + ("GetSourcePath", c_void_p), + ("GetSourcePathElement", c_void_p), + ("SetSourcePath", c_void_p), + ("AppendSourcePath", c_void_p), + ("FindSourceFile", c_void_p), + ("GetSourceFileLineOffsets", c_void_p), + ("GetModuleVersionInformation", c_void_p), + ("GetModuleNameString", ids_getmodulenamestring), + ("GetConstantName", c_void_p), + ("GetFieldName", c_void_p), + ("GetTypeOptions", c_void_p), + ("AddTypeOptions", c_void_p), + ("RemoveTypeOptions", c_void_p), + ("SetTypeOptions", c_void_p), + ("GetNameByOffsetWide", c_void_p), + ("GetOffsetByNameWide", c_void_p), + ("GetNearNameByOffsetWide", c_void_p), + ("GetLineByOffsetWide", c_void_p), + ("GetOffsetByLineWide", c_void_p), + ("GetModuleByModuleNameWide", c_void_p), + ("GetSymbolModuleWide", c_void_p), + ("GetTypeNameWide", c_void_p), + ("GetTypeIdWide", c_void_p), + ("GetFieldOffsetWide", c_void_p), + ("GetSymbolTypeIdWide", c_void_p), + ("GetScopeSymbolGroup2", ids_getscopesymbolgroup2), + ("CreateSymbolGroup2", c_void_p), + ("StartSymbolMatchWide", c_void_p), + ("GetNextSymbolMatchWide", c_void_p), + ("ReloadWide", c_void_p), + ("GetSymbolPathWide", c_void_p), + ("SetSymbolPathWide", c_void_p), + ("AppendSymbolPathWide", c_void_p), + ("GetImagePathWide", c_void_p), + ("SetImagePathWide", c_void_p), + ("AppendImagePathWide", c_void_p), + ("GetSourcePathWide", c_void_p), + ("GetSourcePathElementWide", c_void_p), + ("SetSourcePathWide", c_void_p), + ("AppendSourcePathWide", c_void_p), + ("FindSourceFileWide", c_void_p), + ("GetSourceFileLineOffsetsWide", c_void_p), + ("GetModuleVersionInformationWide", c_void_p), + ("GetModuleNameStringWide", c_void_p), + ("GetConstantNameWide", c_void_p), + ("GetFieldNameWide", c_void_p), + ("IsManagedModule", c_void_p), + ("GetModuleByModuleName2", c_void_p), + ("GetModuleByModuleName2Wide", c_void_p), + ("GetModuleByOffset2", c_void_p), + ("AddSyntheticModule", c_void_p), + ("AddSyntheticModuleWide", c_void_p), + ("RemoveSyntheticModule", c_void_p), + ("GetCurrentScopeFrameIndex", ids_getcurrentscopeframeindex), + ("SetScopeFrameByIndex", ids_setscopeframebyindex), + ("SetScopeFromJitDebugInfo", c_void_p), + ("SetScopeFromStoredEvent", c_void_p), + ("OutputSymbolByOffset", c_void_p), + ("GetFunctionEntryByOffset", c_void_p), + ("GetFieldTypeAndOffset", c_void_p), + ("GetFieldTypeAndOffsetWide", c_void_p), + ("AddSyntheticSymbol", c_void_p), + ("AddSyntheticSymbolWide", c_void_p), + ("RemoveSyntheticSymbol", c_void_p), + ("GetSymbolEntriesByOffset", c_void_p), + ("GetSymbolEntriesByName", ids_getsymbolentriesbyname), + ("GetSymbolEntriesByNameWide", c_void_p), + ("GetSymbolEntryByToken", c_void_p), + ("GetSymbolEntryInformation", ids_getsymbolentryinformation), + ("GetSymbolEntryString", ids_getsymbolentrystring), + ("GetSymbolEntryStringWide", c_void_p), + ("GetSymbolEntryOffsetRegions", c_void_p), + ("GetSymbolEntryBySymbolEntry", c_void_p), + ("GetSourceEntriesByOffset", c_void_p), + ("GetSourceEntriesByLine", c_void_p), + ("GetSourceEntriesByLineWide", c_void_p), + ("GetSourceEntryString", c_void_p), + ("GetSourceEntryStringWide", c_void_p), + ("GetSourceEntryOffsetRegions", c_void_p), + ("GetsourceEntryBySourceEntry", c_void_p), + ("GetScopeEx", c_void_p), + ("SetScopeEx", c_void_p), + ("GetNameByInlineContext", ids_getnamebyinlinecontext), + ("GetNameByInlineContextWide", c_void_p), + ("GetLineByInlineContext", ids_getlinebyinlinecontext), + ("GetLineByInlineContextWide", c_void_p), + ("OutputSymbolByInlineContext", c_void_p), + ("GetCurrentScopeFrameIndexEx", c_void_p), + ("SetScopeFrameByIndexEx", c_void_p) + ] + +IDebugSymbols5._fields_ = [("lpVtbl", POINTER(IDebugSymbols5Vtbl))] + +SymbolId = namedtuple("SymbolId", ["ModuleBase", "Id"]) +SymbolEntry = namedtuple("SymbolEntry", ["ModuleBase", "Offset", "Id", "Arg64", "Size", "Flags", "TypeId", "NameSize", "Token", "Tag", "Arg32"]) +DebugModuleParams = namedtuple("DebugModuleParams", ["Base", "Size", "TimeDateStamp", "Checksum", "Flags", "SymbolType", "ImageNameSize", "ModuleNameSize", "LoadedImageNameSize", "SymbolFileNameSize", "MappedImageNameSize"]) + +class SymTags(IntEnum): + Null = 0 + Exe = 1 + SymTagFunction = 5 + +def make_debug_module_params(cdata): + fieldvalues = map(lambda y: getattr(cdata, y), DebugModuleParams._fields) + return DebugModuleParams(*fieldvalues) + +class Symbols(object): + def __init__(self, symbols): + self.ptr = symbols + self.symbols = symbols.contents + self.vt = self.symbols.lpVtbl.contents + # Keep some handy ulongs for passing into C methods. + self.ulong = c_ulong() + self.ulong64 = c_ulonglong() + + def GetCurrentScopeFrameIndex(self): + res = self.vt.GetCurrentScopeFrameIndex(self.symbols, byref(self.ulong)) + aborter(res, "GetCurrentScopeFrameIndex") + return self.ulong.value + + def SetScopeFrameByIndex(self, idx): + res = self.vt.SetScopeFrameByIndex(self.symbols, idx) + aborter(res, "SetScopeFrameByIndex", ignore=[E_EINVAL]) + return res != E_EINVAL + + def GetOffsetByName(self, name): + res = self.vt.GetOffsetByName(self.symbols, name.encode("ascii"), byref(self.ulong64)) + aborter(res, "GetOffsetByName {}".format(name)) + return self.ulong64.value + + def GetNearNameByOffset(self, addr): + ptr = create_string_buffer(256) + pulong = c_ulong() + disp = c_ulonglong() + # Zero arg -> "delta" indicating how many symbols to skip + res = self.vt.GetNearNameByOffset(self.symbols, addr, 0, ptr, 255, byref(pulong), byref(disp)) + if res == E_NOINTERFACE: + return "{noname}" + aborter(res, "GetNearNameByOffset") + ptr[255] = '\0'.encode("ascii") + return '{}+{}'.format(string_at(ptr).decode("ascii"), disp.value) + + def GetModuleByModuleName2(self, name): + # First zero arg -> module index to search from, second zero arg -> + # DEBUG_GETMOD_* flags, none of which we use. + res = self.vt.GetModuleByModuleName2(self.symbols, name, 0, 0, None, byref(self.ulong64)) + aborter(res, "GetModuleByModuleName2") + return self.ulong64.value + + def GetScopeSymbolGroup2(self): + retptr = POINTER(IDebugSymbolGroup2)() + res = self.vt.GetScopeSymbolGroup2(self.symbols, ScopeGroupFlags.DEBUG_SCOPE_GROUP_ALL, None, retptr) + aborter(res, "GetScopeSymbolGroup2") + return SymbolGroup(retptr) + + def GetSymbolEntryString(self, idx, module): + symid = DEBUG_MODULE_AND_ID() + symid.ModuleBase = module + symid.Id = idx + ptr = create_string_buffer(1024) + # Zero arg is the string index -- symbols can have multiple names, for now + # only support the first one. + res = self.vt.GetSymbolEntryString(self.symbols, symid, 0, ptr, 1023, byref(self.ulong)) + aborter(res, "GetSymbolEntryString") + return string_at(ptr).decode("ascii") + + def GetSymbolEntryInformation(self, module, theid): + symid = DEBUG_MODULE_AND_ID() + symentry = DEBUG_SYMBOL_ENTRY() + symid.ModuleBase = module + symid.Id = theid + res = self.vt.GetSymbolEntryInformation(self.symbols, symid, symentry) + aborter(res, "GetSymbolEntryInformation") + # Fetch fields into SymbolEntry object + fields = map(lambda x: getattr(symentry, x), SymbolEntry._fields) + return SymbolEntry(*fields) + + def GetSymbolEntriesByName(self, symstr): + # Initial query to find number of symbol entries + res = self.vt.GetSymbolEntriesByName(self.symbols, symstr.encode("ascii"), 0, None, 0, byref(self.ulong)) + aborter(res, "GetSymbolEntriesByName") + + # Build a buffer and query for 'length' entries + length = self.ulong.value + symrecs = (DEBUG_MODULE_AND_ID * length)() + # Zero arg -> flags, of which there are none defined. + res = self.vt.GetSymbolEntriesByName(self.symbols, symstr.encode("ascii"), 0, symrecs, length, byref(self.ulong)) + aborter(res, "GetSymbolEntriesByName") + + # Extract 'length' number of SymbolIds + length = self.ulong.value + def extract(x): + sym = symrecs[x] + return SymbolId(sym.ModuleBase, sym.Id) + return [extract(x) for x in range(length)] + + def GetSymbolPath(self): + # Query for length of buffer to allocate + res = self.vt.GetSymbolPath(self.symbols, None, 0, byref(self.ulong)) + aborter(res, "GetSymbolPath", ignore=[S_FALSE]) + + # Fetch 'length' length symbol path string + length = self.ulong.value + arr = create_string_buffer(length) + res = self.vt.GetSymbolPath(self.symbols, arr, length, byref(self.ulong)) + aborter(res, "GetSymbolPath") + + return string_at(arr).decode("ascii") + + def GetSourcePath(self): + # Query for length of buffer to allocate + res = self.vt.GetSourcePath(self.symbols, None, 0, byref(self.ulong)) + aborter(res, "GetSourcePath", ignore=[S_FALSE]) + + # Fetch a string of len 'length' + length = self.ulong.value + arr = create_string_buffer(length) + res = self.vt.GetSourcePath(self.symbols, arr, length, byref(self.ulong)) + aborter(res, "GetSourcePath") + + return string_at(arr).decode("ascii") + + def SetSourcePath(self, string): + res = self.vt.SetSourcePath(self.symbols, string.encode("ascii")) + aborter(res, "SetSourcePath") + return + + def GetModuleParameters(self, base): + self.ulong64.value = base + params = DEBUG_MODULE_PARAMETERS() + # Fetch one module params struct, starting at idx zero + res = self.vt.GetModuleParameters(self.symbols, 1, byref(self.ulong64), 0, byref(params)) + aborter(res, "GetModuleParameters") + return make_debug_module_params(params) + + def GetSymbolOptions(self): + res = self.vt.GetSymbolOptions(self.symbols, byref(self.ulong)) + aborter(res, "GetSymbolOptions") + return SymbolOptionFlags(self.ulong.value) + + def SetSymbolOptions(self, opts): + assert isinstance(opts, SymbolOptionFlags) + res = self.vt.SetSymbolOptions(self.symbols, opts.value) + aborter(res, "SetSymbolOptions") + return + + def GetLineByOffset(self, offs): + # Initial query for filename buffer size + res = self.vt.GetLineByOffset(self.symbols, offs, None, None, 0, byref(self.ulong), None) + if res == E_FAIL: + return None # Sometimes we just can't get line numbers, of course + aborter(res, "GetLineByOffset", ignore=[S_FALSE]) + + # Allocate filename buffer and query for line number too + filenamelen = self.ulong.value + text = create_string_buffer(filenamelen) + line = c_ulong() + res = self.vt.GetLineByOffset(self.symbols, offs, byref(line), text, filenamelen, byref(self.ulong), None) + aborter(res, "GetLineByOffset") + + return string_at(text).decode("ascii"), line.value + + def GetModuleNameString(self, whichname, base): + # Initial query for name string length + res = self.vt.GetModuleNameString(self.symbols, whichname, DEBUG_ANY_ID, base, None, 0, byref(self.ulong)) + aborter(res, "GetModuleNameString", ignore=[S_FALSE]) + + module_name_len = self.ulong.value + module_name = (c_char * module_name_len)() + res = self.vt.GetModuleNameString(self.symbols, whichname, DEBUG_ANY_ID, base, module_name, module_name_len, None) + aborter(res, "GetModuleNameString") + + return string_at(module_name).decode("ascii") + + def GetNameByInlineContext(self, pc, ctx): + # None args -> ignore output name size and displacement + buf = create_string_buffer(256) + res = self.vt.GetNameByInlineContext(self.symbols, pc, ctx, buf, 255, None, None) + aborter(res, "GetNameByInlineContext") + return string_at(buf).decode("ascii") + + def GetLineByInlineContext(self, pc, ctx): + # None args -> ignore output filename size and displacement + buf = create_string_buffer(256) + res = self.vt.GetLineByInlineContext(self.symbols, pc, ctx, byref(self.ulong), buf, 255, None, None) + aborter(res, "GetLineByInlineContext") + return string_at(buf).decode("ascii"), self.ulong.value + + def get_all_symbols(self): + main_module_name = self.get_exefile_module_name() + idnumbers = self.GetSymbolEntriesByName("{}!*".format(main_module_name)) + lst = [] + for symid in idnumbers: + s = self.GetSymbolEntryString(symid.Id, symid.ModuleBase) + symentry = self.GetSymbolEntryInformation(symid.ModuleBase, symid.Id) + lst.append((s, symentry)) + return lst + + def get_all_functions(self): + syms = self.get_all_symbols() + return [x for x in syms if x[1].Tag == SymTags.SymTagFunction] + + def get_all_modules(self): + params = DEBUG_MODULE_PARAMETERS() + idx = 0 + res = 0 + all_modules = [] + while res != E_EINVAL: + res = self.vt.GetModuleParameters(self.symbols, 1, None, idx, byref(params)) + aborter(res, "GetModuleParameters", ignore=[E_EINVAL]) + all_modules.append(make_debug_module_params(params)) + idx += 1 + return all_modules + + def get_exefile_module(self): + all_modules = self.get_all_modules() + reduce_func = lambda x, y: y if y.Flags & DebugModuleFlags.DEBUG_MODULE_EXE_MODULE else x + main_module = reduce(reduce_func, all_modules, None) + if main_module is None: + raise Exception("Couldn't find the exefile module") + return main_module + + def get_module_name(self, base): + return self.GetModuleNameString(DebugModuleNames.DEBUG_MODNAME_MODULE, base) + + def get_exefile_module_name(self): + return self.get_module_name(self.get_exefile_module().Base) diff --git a/debuginfo-tests/dexter/dex/debugger/dbgeng/symgroup.py b/debuginfo-tests/dexter/dex/debugger/dbgeng/symgroup.py new file mode 100644 index 00000000000..2775af3279b --- /dev/null +++ b/debuginfo-tests/dexter/dex/debugger/dbgeng/symgroup.py @@ -0,0 +1,98 @@ +# 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 collections import namedtuple +from ctypes import * +from functools import partial + +from .utils import * + +Symbol = namedtuple("Symbol", ["num", "name", "type", "value"]) + +class IDebugSymbolGroup2(Structure): + pass + +class IDebugSymbolGroup2Vtbl(Structure): + wrp = partial(WINFUNCTYPE, c_long, POINTER(IDebugSymbolGroup2)) + ids_getnumbersymbols = wrp(c_ulong_p) + ids_getsymbolname = wrp(c_ulong, c_char_p, c_ulong, c_ulong_p) + ids_getsymboltypename = wrp(c_ulong, c_char_p, c_ulong, c_ulong_p) + ids_getsymbolvaluetext = wrp(c_ulong, c_char_p, c_ulong, c_ulong_p) + _fields_ = [ + ("QueryInterface", c_void_p), + ("AddRef", c_void_p), + ("Release", c_void_p), + ("GetNumberSymbols", ids_getnumbersymbols), + ("AddSymbol", c_void_p), + ("RemoveSymbolByName", c_void_p), + ("RemoveSymbolByIndex", c_void_p), + ("GetSymbolName", ids_getsymbolname), + ("GetSymbolParameters", c_void_p), + ("ExpandSymbol", c_void_p), + ("OutputSymbols", c_void_p), + ("WriteSymbol", c_void_p), + ("OutputAsType", c_void_p), + ("AddSymbolWide", c_void_p), + ("RemoveSymbolByNameWide", c_void_p), + ("GetSymbolNameWide", c_void_p), + ("WritesymbolWide", c_void_p), + ("OutputAsTypeWide", c_void_p), + ("GetSymbolTypeName", ids_getsymboltypename), + ("GetSymbolTypeNameWide", c_void_p), + ("GetSymbolSize", c_void_p), + ("GetSymbolOffset", c_void_p), + ("GetSymbolRegister", c_void_p), + ("GetSymbolValueText", ids_getsymbolvaluetext), + ("GetSymbolValueTextWide", c_void_p), + ("GetSymbolEntryInformation", c_void_p) + ] + +IDebugSymbolGroup2._fields_ = [("lpVtbl", POINTER(IDebugSymbolGroup2Vtbl))] + +class SymbolGroup(object): + def __init__(self, symgroup): + self.symgroup = symgroup.contents + self.vt = self.symgroup.lpVtbl.contents + self.ulong = c_ulong() + + def GetNumberSymbols(self): + res = self.vt.GetNumberSymbols(self.symgroup, byref(self.ulong)) + aborter(res, "GetNumberSymbols") + return self.ulong.value + + def GetSymbolName(self, idx): + buf = create_string_buffer(256) + res = self.vt.GetSymbolName(self.symgroup, idx, buf, 255, byref(self.ulong)) + aborter(res, "GetSymbolName") + thelen = self.ulong.value + return string_at(buf).decode("ascii") + + def GetSymbolTypeName(self, idx): + buf = create_string_buffer(256) + res = self.vt.GetSymbolTypeName(self.symgroup, idx, buf, 255, byref(self.ulong)) + aborter(res, "GetSymbolTypeName") + thelen = self.ulong.value + return string_at(buf).decode("ascii") + + def GetSymbolValueText(self, idx, handleserror=False): + buf = create_string_buffer(256) + res = self.vt.GetSymbolValueText(self.symgroup, idx, buf, 255, byref(self.ulong)) + if res != 0 and handleserror: + return None + aborter(res, "GetSymbolTypeName") + thelen = self.ulong.value + return string_at(buf).decode("ascii") + + def get_symbol(self, idx): + name = self.GetSymbolName(idx) + thetype = self.GetSymbolTypeName(idx) + value = self.GetSymbolValueText(idx) + return Symbol(idx, name, thetype, value) + + def get_all_symbols(self): + num_syms = self.GetNumberSymbols() + return list(map(self.get_symbol, list(range(num_syms)))) diff --git a/debuginfo-tests/dexter/dex/debugger/dbgeng/sysobjs.py b/debuginfo-tests/dexter/dex/debugger/dbgeng/sysobjs.py new file mode 100644 index 00000000000..0e9844a363b --- /dev/null +++ b/debuginfo-tests/dexter/dex/debugger/dbgeng/sysobjs.py @@ -0,0 +1,200 @@ +# 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 ctypes import * +from functools import partial + +from .utils import * + +# UUID For SystemObjects4 interface. +DebugSystemObjects4IID = IID(0x489468e6, 0x7d0f, 0x4af5, IID_Data4_Type(0x87, 0xab, 0x25, 0x20, 0x74, 0x54, 0xd5, 0x53)) + +class IDebugSystemObjects4(Structure): + pass + +class IDebugSystemObjects4Vtbl(Structure): + wrp = partial(WINFUNCTYPE, c_long, POINTER(IDebugSystemObjects4)) + ids_getnumberprocesses = wrp(POINTER(c_ulong)) + ids_getprocessidsbyindex = wrp(c_ulong, c_ulong, c_ulong_p, c_ulong_p) + ids_setcurrentprocessid = wrp(c_ulong) + ids_getnumberthreads = wrp(c_ulong_p) + ids_getthreadidsbyindex = wrp(c_ulong, c_ulong, c_ulong_p, c_ulong_p) + ids_setcurrentthreadid = wrp(c_ulong) + _fields_ = [ + ("QueryInterface", c_void_p), + ("AddRef", c_void_p), + ("Release", c_void_p), + ("GetEventThread", c_void_p), + ("GetEventProcess", c_void_p), + ("GetCurrentThreadId", c_void_p), + ("SetCurrentThreadId", ids_setcurrentthreadid), + ("GetCurrentProcessId", c_void_p), + ("SetCurrentProcessId", ids_setcurrentprocessid), + ("GetNumberThreads", ids_getnumberthreads), + ("GetTotalNumberThreads", c_void_p), + ("GetThreadIdsByIndex", ids_getthreadidsbyindex), + ("GetThreadIdByProcessor", c_void_p), + ("GetCurrentThreadDataOffset", c_void_p), + ("GetThreadIdByDataOffset", c_void_p), + ("GetCurrentThreadTeb", c_void_p), + ("GetThreadIdByTeb", c_void_p), + ("GetCurrentThreadSystemId", c_void_p), + ("GetThreadIdBySystemId", c_void_p), + ("GetCurrentThreadHandle", c_void_p), + ("GetThreadIdByHandle", c_void_p), + ("GetNumberProcesses", ids_getnumberprocesses), + ("GetProcessIdsByIndex", ids_getprocessidsbyindex), + ("GetCurrentProcessDataOffset", c_void_p), + ("GetProcessIdByDataOffset", c_void_p), + ("GetCurrentProcessPeb", c_void_p), + ("GetProcessIdByPeb", c_void_p), + ("GetCurrentProcessSystemId", c_void_p), + ("GetProcessIdBySystemId", c_void_p), + ("GetCurrentProcessHandle", c_void_p), + ("GetProcessIdByHandle", c_void_p), + ("GetCurrentProcessExecutableName", c_void_p), + ("GetCurrentProcessUpTime", c_void_p), + ("GetImplicitThreadDataOffset", c_void_p), + ("SetImplicitThreadDataOffset", c_void_p), + ("GetImplicitProcessDataOffset", c_void_p), + ("SetImplicitProcessDataOffset", c_void_p), + ("GetEventSystem", c_void_p), + ("GetCurrentSystemId", c_void_p), + ("SetCurrentSystemId", c_void_p), + ("GetNumberSystems", c_void_p), + ("GetSystemIdsByIndex", c_void_p), + ("GetTotalNumberThreadsAndProcesses", c_void_p), + ("GetCurrentSystemServer", c_void_p), + ("GetSystemByServer", c_void_p), + ("GetCurrentSystemServerName", c_void_p), + ("GetCurrentProcessExecutableNameWide", c_void_p), + ("GetCurrentSystemServerNameWide", c_void_p) + ] + +IDebugSystemObjects4._fields_ = [("lpVtbl", POINTER(IDebugSystemObjects4Vtbl))] + +class SysObjects(object): + def __init__(self, sysobjects): + self.ptr = sysobjects + self.sysobjects = sysobjects.contents + self.vt = self.sysobjects.lpVtbl.contents + # Keep a handy ulong for passing into C methods. + self.ulong = c_ulong() + + def GetNumberSystems(self): + res = self.vt.GetNumberSystems(self.sysobjects, byref(self.ulong)) + aborter(res, "GetNumberSystems") + return self.ulong.value + + def GetNumberProcesses(self): + res = self.vt.GetNumberProcesses(self.sysobjects, byref(self.ulong)) + aborter(res, "GetNumberProcesses") + return self.ulong.value + + def GetNumberThreads(self): + res = self.vt.GetNumberThreads(self.sysobjects, byref(self.ulong)) + aborter(res, "GetNumberThreads") + return self.ulong.value + + def GetTotalNumberThreadsAndProcesses(self): + tthreads = c_ulong() + tprocs = c_ulong() + pulong3 = c_ulong() + res = self.vt.GetTotalNumberThreadsAndProcesses(self.sysobjects, byref(tthreads), byref(tprocs), byref(pulong3), byref(pulong3), byref(pulong3)) + aborter(res, "GettotalNumberThreadsAndProcesses") + return tthreads.value, tprocs.value + + def GetCurrentProcessId(self): + res = self.vt.GetCurrentProcessId(self.sysobjects, byref(self.ulong)) + aborter(res, "GetCurrentProcessId") + return self.ulong.value + + def SetCurrentProcessId(self, sysid): + res = self.vt.SetCurrentProcessId(self.sysobjects, sysid) + aborter(res, "SetCurrentProcessId") + return + + def GetCurrentThreadId(self): + res = self.vt.GetCurrentThreadId(self.sysobjects, byref(self.ulong)) + aborter(res, "GetCurrentThreadId") + return self.ulong.value + + def SetCurrentThreadId(self, sysid): + res = self.vt.SetCurrentThreadId(self.sysobjects, sysid) + aborter(res, "SetCurrentThreadId") + return + + def GetProcessIdsByIndex(self): + num_processes = self.GetNumberProcesses() + if num_processes == 0: + return [] + engineids = (c_ulong * num_processes)() + pids = (c_ulong * num_processes)() + for x in range(num_processes): + engineids[x] = DEBUG_ANY_ID + pids[x] = DEBUG_ANY_ID + res = self.vt.GetProcessIdsByIndex(self.sysobjects, 0, num_processes, engineids, pids) + aborter(res, "GetProcessIdsByIndex") + return list(zip(engineids, pids)) + + def GetThreadIdsByIndex(self): + num_threads = self.GetNumberThreads() + if num_threads == 0: + return [] + engineids = (c_ulong * num_threads)() + tids = (c_ulong * num_threads)() + for x in range(num_threads): + engineids[x] = DEBUG_ANY_ID + tids[x] = DEBUG_ANY_ID + # Zero -> start index + res = self.vt.GetThreadIdsByIndex(self.sysobjects, 0, num_threads, engineids, tids) + aborter(res, "GetThreadIdsByIndex") + return list(zip(engineids, tids)) + + def GetCurThreadHandle(self): + pulong64 = c_ulonglong() + res = self.vt.GetCurrentThreadHandle(self.sysobjects, byref(pulong64)) + aborter(res, "GetCurrentThreadHandle") + return pulong64.value + + def set_current_thread(self, pid, tid): + proc_sys_id = -1 + for x in self.GetProcessIdsByIndex(): + sysid, procid = x + if procid == pid: + proc_sys_id = sysid + + if proc_sys_id == -1: + raise Exception("Couldn't find designated PID {}".format(pid)) + + self.SetCurrentProcessId(proc_sys_id) + + thread_sys_id = -1 + for x in self.GetThreadIdsByIndex(): + sysid, threadid = x + if threadid == tid: + thread_sys_id = sysid + + if thread_sys_id == -1: + raise Exception("Couldn't find designated TID {}".format(tid)) + + self.SetCurrentThreadId(thread_sys_id) + return + + def print_current_procs_threads(self): + procs = [] + for x in self.GetProcessIdsByIndex(): + sysid, procid = x + procs.append(procid) + + threads = [] + for x in self.GetThreadIdsByIndex(): + sysid, threadid = x + threads.append(threadid) + + print("Current processes: {}".format(procs)) + print("Current threads: {}".format(threads)) diff --git a/debuginfo-tests/dexter/dex/debugger/dbgeng/utils.py b/debuginfo-tests/dexter/dex/debugger/dbgeng/utils.py new file mode 100644 index 00000000000..0c9197aa1c9 --- /dev/null +++ b/debuginfo-tests/dexter/dex/debugger/dbgeng/utils.py @@ -0,0 +1,47 @@ +# 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 ctypes import * + +# Error codes are negative when received by python, but are typically +# represented by unsigned hex elsewhere. Subtract 2^32 from the unsigned +# hex to produce negative error codes. +E_NOINTERFACE = 0x80004002 - 0x100000000 +E_FAIL = 0x80004005 - 0x100000000 +E_EINVAL = 0x80070057 - 0x100000000 +E_INTERNALEXCEPTION = 0x80040205 - 0x100000000 +S_FALSE = 1 + +# This doesn't fit into any convenient category +DEBUG_ANY_ID = 0xFFFFFFFF + +class WinError(Exception): + def __init__(self, msg, hstatus): + self.hstatus = hstatus + super(WinError, self).__init__(msg) + +def aborter(res, msg, ignore=[]): + if res != 0 and res not in ignore: + # Convert a negative error code to a positive unsigned one, which is + # now NTSTATUSes appear in documentation. + if res < 0: + res += 0x100000000 + msg = '{:08X} : {}'.format(res, msg) + raise WinError(msg, res) + +IID_Data4_Type = c_ubyte * 8 + +class IID(Structure): + _fields_ = [ + ("Data1", c_uint), + ("Data2", c_ushort), + ("Data3", c_ushort), + ("Data4", IID_Data4_Type) + ] + +c_ulong_p = POINTER(c_ulong) +c_ulong64_p = POINTER(c_ulonglong) diff --git a/debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py b/debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py new file mode 100644 index 00000000000..425d3c2adb1 --- /dev/null +++ b/debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py @@ -0,0 +1,244 @@ +# 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 +"""Interface for communicating with the LLDB debugger via its python interface. +""" + +import imp +import os +from subprocess import CalledProcessError, check_output, STDOUT +import sys + +from dex.debugger.DebuggerBase import DebuggerBase +from dex.dextIR import FrameIR, LocIR, StepIR, StopReason, ValueIR +from dex.dextIR import StackFrame, SourceLocation, ProgramState +from dex.utils.Exceptions import DebuggerException, LoadDebuggerException +from dex.utils.ReturnCode import ReturnCode + + +class LLDB(DebuggerBase): + def __init__(self, context, *args): + self.lldb_executable = context.options.lldb_executable + self._debugger = None + self._target = None + self._process = None + self._thread = None + super(LLDB, self).__init__(context, *args) + + def _custom_init(self): + self._debugger = self._interface.SBDebugger.Create() + self._debugger.SetAsync(False) + self._target = self._debugger.CreateTargetWithFileAndArch( + self.context.options.executable, self.context.options.arch) + if not self._target: + raise LoadDebuggerException( + 'could not create target for executable "{}" with arch:{}'. + format(self.context.options.executable, + self.context.options.arch)) + + def _custom_exit(self): + if getattr(self, '_process', None): + self._process.Kill() + if getattr(self, '_debugger', None) and getattr(self, '_target', None): + self._debugger.DeleteTarget(self._target) + + def _translate_stop_reason(self, reason): + if reason == self._interface.eStopReasonNone: + return None + if reason == self._interface.eStopReasonBreakpoint: + return StopReason.BREAKPOINT + if reason == self._interface.eStopReasonPlanComplete: + return StopReason.STEP + if reason == self._interface.eStopReasonThreadExiting: + return StopReason.PROGRAM_EXIT + if reason == self._interface.eStopReasonException: + return StopReason.ERROR + return StopReason.OTHER + + def _load_interface(self): + try: + args = [self.lldb_executable, '-P'] + pythonpath = check_output( + args, stderr=STDOUT).rstrip().decode('utf-8') + except CalledProcessError as e: + raise LoadDebuggerException(str(e), sys.exc_info()) + except OSError as e: + raise LoadDebuggerException( + '{} ["{}"]'.format(e.strerror, self.lldb_executable), + sys.exc_info()) + + if not os.path.isdir(pythonpath): + raise LoadDebuggerException( + 'path "{}" does not exist [result of {}]'.format( + pythonpath, args), sys.exc_info()) + + try: + module_info = imp.find_module('lldb', [pythonpath]) + return imp.load_module('lldb', *module_info) + except ImportError as e: + msg = str(e) + if msg.endswith('not a valid Win32 application.'): + msg = '{} [Are you mixing 32-bit and 64-bit binaries?]'.format( + msg) + raise LoadDebuggerException(msg, sys.exc_info()) + + @classmethod + def get_name(cls): + return 'lldb' + + @classmethod + def get_option_name(cls): + return 'lldb' + + @property + def version(self): + try: + return self._interface.SBDebugger_GetVersionString() + except AttributeError: + return None + + def clear_breakpoints(self): + self._target.DeleteAllBreakpoints() + + def add_breakpoint(self, file_, line): + if not self._target.BreakpointCreateByLocation(file_, line): + raise LoadDebuggerException( + 'could not add breakpoint [{}:{}]'.format(file_, line)) + + def launch(self): + self._process = self._target.LaunchSimple(None, None, os.getcwd()) + if not self._process or self._process.GetNumThreads() == 0: + raise DebuggerException('could not launch process') + if self._process.GetNumThreads() != 1: + raise DebuggerException('multiple threads not supported') + self._thread = self._process.GetThreadAtIndex(0) + assert self._thread, (self._process, self._thread) + + def step(self): + self._thread.StepInto() + + def go(self) -> ReturnCode: + self._process.Continue() + return ReturnCode.OK + + def get_step_info(self): + frames = [] + state_frames = [] + + for i in range(0, self._thread.GetNumFrames()): + sb_frame = self._thread.GetFrameAtIndex(i) + sb_line = sb_frame.GetLineEntry() + sb_filespec = sb_line.GetFileSpec() + + try: + path = os.path.join(sb_filespec.GetDirectory(), + sb_filespec.GetFilename()) + except (AttributeError, TypeError): + path = None + + function = self._sanitize_function_name(sb_frame.GetFunctionName()) + + loc_dict = { + 'path': path, + 'lineno': sb_line.GetLine(), + 'column': sb_line.GetColumn() + } + loc = LocIR(**loc_dict) + + frame = FrameIR( + function=function, is_inlined=sb_frame.IsInlined(), loc=loc) + + if any( + name in (frame.function or '') # pylint: disable=no-member + for name in self.frames_below_main): + break + + frames.append(frame) + + state_frame = StackFrame(function=frame.function, + is_inlined=frame.is_inlined, + location=SourceLocation(**loc_dict), + watches={}) + for expr in map( + lambda watch, idx=i: self.evaluate_expression(watch, idx), + self.watches): + state_frame.watches[expr.expression] = expr + state_frames.append(state_frame) + + if len(frames) == 1 and frames[0].function is None: + frames = [] + state_frames = [] + + reason = self._translate_stop_reason(self._thread.GetStopReason()) + + return StepIR( + step_index=self.step_index, frames=frames, stop_reason=reason, + program_state=ProgramState(state_frames)) + + @property + def is_running(self): + # We're not running in async mode so this is always False. + return False + + @property + def is_finished(self): + return not self._thread.GetFrameAtIndex(0) + + @property + def frames_below_main(self): + return ['__scrt_common_main_seh', '__libc_start_main'] + + def evaluate_expression(self, expression, frame_idx=0) -> ValueIR: + result = self._thread.GetFrameAtIndex(frame_idx + ).EvaluateExpression(expression) + error_string = str(result.error) + + value = result.value + could_evaluate = not any(s in error_string for s in [ + "Can't run the expression locally", + "use of undeclared identifier", + "no member named", + "Couldn't lookup symbols", + "reference to local variable", + "invalid use of 'this' outside of a non-static member function", + ]) + + is_optimized_away = any(s in error_string for s in [ + 'value may have been optimized out', + ]) + + is_irretrievable = any(s in error_string for s in [ + "couldn't get the value of variable", + "couldn't read its memory", + "couldn't read from memory", + "Cannot access memory at address", + "invalid address (fault address:", + ]) + + if could_evaluate and not is_irretrievable and not is_optimized_away: + assert error_string == 'success', (error_string, expression, value) + # assert result.value is not None, (result.value, expression) + + if error_string == 'success': + error_string = None + + # attempt to find expression as a variable, if found, take the variable + # obj's type information as it's 'usually' more accurate. + var_result = self._thread.GetFrameAtIndex(frame_idx).FindVariable(expression) + if str(var_result.error) == 'success': + type_name = var_result.type.GetDisplayTypeName() + else: + type_name = result.type.GetDisplayTypeName() + + return ValueIR( + expression=expression, + value=value, + type_name=type_name, + error_string=error_string, + could_evaluate=could_evaluate, + is_optimized_away=is_optimized_away, + is_irretrievable=is_irretrievable, + ) diff --git a/debuginfo-tests/dexter/dex/debugger/lldb/__init__.py b/debuginfo-tests/dexter/dex/debugger/lldb/__init__.py new file mode 100644 index 00000000000..1282f2ddc90 --- /dev/null +++ b/debuginfo-tests/dexter/dex/debugger/lldb/__init__.py @@ -0,0 +1,8 @@ +# 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.debugger.lldb.LLDB import LLDB diff --git a/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py b/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py new file mode 100644 index 00000000000..596dc31ab4a --- /dev/null +++ b/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py @@ -0,0 +1,224 @@ +# 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 +"""Interface for communicating with the Visual Studio debugger via DTE.""" + +import abc +import imp +import os +import sys + +from dex.debugger.DebuggerBase import DebuggerBase +from dex.dextIR import FrameIR, LocIR, StepIR, StopReason, ValueIR +from dex.dextIR import StackFrame, SourceLocation, ProgramState +from dex.utils.Exceptions import Error, LoadDebuggerException +from dex.utils.ReturnCode import ReturnCode + + +def _load_com_module(): + try: + module_info = imp.find_module( + 'ComInterface', + [os.path.join(os.path.dirname(__file__), 'windows')]) + return imp.load_module('ComInterface', *module_info) + except ImportError as e: + raise LoadDebuggerException(e, sys.exc_info()) + + +class VisualStudio(DebuggerBase, metaclass=abc.ABCMeta): # pylint: disable=abstract-method + + # Constants for results of Debugger.CurrentMode + # (https://msdn.microsoft.com/en-us/library/envdte.debugger.currentmode.aspx) + dbgDesignMode = 1 + dbgBreakMode = 2 + dbgRunMode = 3 + + def __init__(self, *args): + self.com_module = None + self._debugger = None + self._solution = None + self._fn_step = None + self._fn_go = None + super(VisualStudio, self).__init__(*args) + + def _custom_init(self): + try: + self._debugger = self._interface.Debugger + self._debugger.HexDisplayMode = False + + self._interface.MainWindow.Visible = ( + self.context.options.show_debugger) + + self._solution = self._interface.Solution + self._solution.Create(self.context.working_directory.path, + 'DexterSolution') + + try: + self._solution.AddFromFile(self._project_file) + except OSError: + raise LoadDebuggerException( + 'could not debug the specified executable', sys.exc_info()) + + self._fn_step = self._debugger.StepInto + self._fn_go = self._debugger.Go + + except AttributeError as e: + raise LoadDebuggerException(str(e), sys.exc_info()) + + def _custom_exit(self): + if self._interface: + self._interface.Quit() + + @property + def _project_file(self): + return self.context.options.executable + + @abc.abstractproperty + def _dte_version(self): + pass + + @property + def _location(self): + bp = self._debugger.BreakpointLastHit + return { + 'path': getattr(bp, 'File', None), + 'lineno': getattr(bp, 'FileLine', None), + 'column': getattr(bp, 'FileColumn', None) + } + + @property + def _mode(self): + return self._debugger.CurrentMode + + def _load_interface(self): + self.com_module = _load_com_module() + return self.com_module.DTE(self._dte_version) + + @property + def version(self): + try: + return self._interface.Version + except AttributeError: + return None + + def clear_breakpoints(self): + for bp in self._debugger.Breakpoints: + bp.Delete() + + def add_breakpoint(self, file_, line): + self._debugger.Breakpoints.Add('', file_, line) + + def launch(self): + self.step() + + def step(self): + self._fn_step() + + def go(self) -> ReturnCode: + self._fn_go() + return ReturnCode.OK + + def set_current_stack_frame(self, idx: int = 0): + thread = self._debugger.CurrentThread + stack_frames = thread.StackFrames + try: + stack_frame = stack_frames[idx] + self._debugger.CurrentStackFrame = stack_frame.raw + except IndexError: + raise Error('attempted to access stack frame {} out of {}' + .format(idx, len(stack_frames))) + + def get_step_info(self): + thread = self._debugger.CurrentThread + stackframes = thread.StackFrames + + frames = [] + state_frames = [] + + + for idx, sf in enumerate(stackframes): + frame = FrameIR( + function=self._sanitize_function_name(sf.FunctionName), + is_inlined=sf.FunctionName.startswith('[Inline Frame]'), + loc=LocIR(path=None, lineno=None, column=None)) + + fname = frame.function or '' # pylint: disable=no-member + if any(name in fname for name in self.frames_below_main): + break + + + state_frame = StackFrame(function=frame.function, + is_inlined=frame.is_inlined, + watches={}) + + for watch in self.watches: + state_frame.watches[watch] = self.evaluate_expression( + watch, idx) + + + state_frames.append(state_frame) + frames.append(frame) + + loc = LocIR(**self._location) + if frames: + frames[0].loc = loc + state_frames[0].location = SourceLocation(**self._location) + + reason = StopReason.BREAKPOINT + if loc.path is None: # pylint: disable=no-member + reason = StopReason.STEP + + program_state = ProgramState(frames=state_frames) + + return StepIR( + step_index=self.step_index, frames=frames, stop_reason=reason, + program_state=program_state) + + @property + def is_running(self): + return self._mode == VisualStudio.dbgRunMode + + @property + def is_finished(self): + return self._mode == VisualStudio.dbgDesignMode + + @property + def frames_below_main(self): + return [ + '[Inline Frame] invoke_main', '__scrt_common_main_seh', + '__tmainCRTStartup', 'mainCRTStartup' + ] + + def evaluate_expression(self, expression, frame_idx=0) -> ValueIR: + self.set_current_stack_frame(frame_idx) + result = self._debugger.GetExpression(expression) + self.set_current_stack_frame(0) + value = result.Value + + is_optimized_away = any(s in value for s in [ + 'Variable is optimized away and not available', + 'Value is not available, possibly due to optimization', + ]) + + is_irretrievable = any(s in value for s in [ + '???', + '<Unable to read memory>', + ]) + + # an optimized away value is still counted as being able to be + # evaluated. + could_evaluate = (result.IsValidValue or is_optimized_away + or is_irretrievable) + + return ValueIR( + expression=expression, + value=value, + type_name=result.Type, + error_string=None, + is_optimized_away=is_optimized_away, + could_evaluate=could_evaluate, + is_irretrievable=is_irretrievable, + ) diff --git a/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio2015.py b/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio2015.py new file mode 100644 index 00000000000..af6edcd2451 --- /dev/null +++ b/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio2015.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 +"""Specializations for the Visual Studio 2015 interface.""" + +from dex.debugger.visualstudio.VisualStudio import VisualStudio + + +class VisualStudio2015(VisualStudio): + @classmethod + def get_name(cls): + return 'Visual Studio 2015' + + @classmethod + def get_option_name(cls): + return 'vs2015' + + @property + def _dte_version(self): + return 'VisualStudio.DTE.14.0' diff --git a/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio2017.py b/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio2017.py new file mode 100644 index 00000000000..f2f757546f3 --- /dev/null +++ b/debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio2017.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 +"""Specializations for the Visual Studio 2017 interface.""" + +from dex.debugger.visualstudio.VisualStudio import VisualStudio + + +class VisualStudio2017(VisualStudio): + @classmethod + def get_name(cls): + return 'Visual Studio 2017' + + @classmethod + def get_option_name(cls): + return 'vs2017' + + @property + def _dte_version(self): + return 'VisualStudio.DTE.15.0' diff --git a/debuginfo-tests/dexter/dex/debugger/visualstudio/__init__.py b/debuginfo-tests/dexter/dex/debugger/visualstudio/__init__.py new file mode 100644 index 00000000000..35fefacf22f --- /dev/null +++ b/debuginfo-tests/dexter/dex/debugger/visualstudio/__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.debugger.visualstudio.VisualStudio2015 import VisualStudio2015 +from dex.debugger.visualstudio.VisualStudio2017 import VisualStudio2017 diff --git a/debuginfo-tests/dexter/dex/debugger/visualstudio/windows/ComInterface.py b/debuginfo-tests/dexter/dex/debugger/visualstudio/windows/ComInterface.py new file mode 100644 index 00000000000..0bce5b533e7 --- /dev/null +++ b/debuginfo-tests/dexter/dex/debugger/visualstudio/windows/ComInterface.py @@ -0,0 +1,119 @@ +# 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 +"""Communication via the Windows COM interface.""" + +import inspect +import time +import sys + +# pylint: disable=import-error +import win32com.client as com +import win32api +# pylint: enable=import-error + +from dex.utils.Exceptions import LoadDebuggerException + +_com_error = com.pywintypes.com_error # pylint: disable=no-member + + +def get_file_version(file_): + try: + info = win32api.GetFileVersionInfo(file_, '\\') + ms = info['FileVersionMS'] + ls = info['FileVersionLS'] + return '.'.join( + str(s) for s in [ + win32api.HIWORD(ms), + win32api.LOWORD(ms), + win32api.HIWORD(ls), + win32api.LOWORD(ls) + ]) + except com.pywintypes.error: # pylint: disable=no-member + return 'no versioninfo present' + + +def _handle_com_error(e): + exc = sys.exc_info() + msg = win32api.FormatMessage(e.hresult) + try: + msg = msg.decode('CP1251') + except AttributeError: + pass + msg = msg.strip() + return msg, exc + + +class ComObject(object): + """Wrap a raw Windows COM object in a class that implements auto-retry of + failed calls. + """ + + def __init__(self, raw): + assert not isinstance(raw, ComObject), raw + self.__dict__['raw'] = raw + + def __str__(self): + return self._call(self.raw.__str__) + + def __getattr__(self, key): + if key in self.__dict__: + return self.__dict__[key] + return self._call(self.raw.__getattr__, key) + + def __setattr__(self, key, val): + if key in self.__dict__: + self.__dict__[key] = val + self._call(self.raw.__setattr__, key, val) + + def __getitem__(self, key): + return self._call(self.raw.__getitem__, key) + + def __setitem__(self, key, val): + self._call(self.raw.__setitem__, key, val) + + def __call__(self, *args): + return self._call(self.raw, *args) + + @classmethod + def _call(cls, fn, *args): + """COM calls tend to randomly fail due to thread sync issues. + The Microsoft recommended solution is to set up a message filter object + to automatically retry failed calls, but this seems prohibitively hard + from python, so this is a custom solution to do the same thing. + All COM accesses should go through this function. + """ + ex = AssertionError("this should never be raised!") + + assert (inspect.isfunction(fn) or inspect.ismethod(fn) + or inspect.isbuiltin(fn)), (fn, type(fn)) + retries = ([0] * 50) + ([1] * 5) + for r in retries: + try: + try: + result = fn(*args) + if inspect.ismethod(result) or 'win32com' in str( + result.__class__): + result = ComObject(result) + return result + except _com_error as e: + msg, _ = _handle_com_error(e) + e = WindowsError(msg) # pylint: disable=undefined-variable + raise e + except (AttributeError, TypeError, OSError) as e: + ex = e + time.sleep(r) + raise ex + + +class DTE(ComObject): + def __init__(self, class_string): + try: + super(DTE, self).__init__(com.DispatchEx(class_string)) + except _com_error as e: + msg, exc = _handle_com_error(e) + raise LoadDebuggerException( + '{} [{}]'.format(msg, class_string), orig_exception=exc) diff --git a/debuginfo-tests/dexter/dex/debugger/visualstudio/windows/__init__.py b/debuginfo-tests/dexter/dex/debugger/visualstudio/windows/__init__.py new file mode 100644 index 00000000000..1194affd891 --- /dev/null +++ b/debuginfo-tests/dexter/dex/debugger/visualstudio/windows/__init__.py @@ -0,0 +1,6 @@ +# 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 diff --git a/debuginfo-tests/dexter/dex/dextIR/BuilderIR.py b/debuginfo-tests/dexter/dex/dextIR/BuilderIR.py new file mode 100644 index 00000000000..b94a1fb7e81 --- /dev/null +++ b/debuginfo-tests/dexter/dex/dextIR/BuilderIR.py @@ -0,0 +1,16 @@ +# 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 BuilderIR: + """Data class which represents the compiler related options passed to Dexter + """ + + def __init__(self, name: str, cflags: str, ldflags: str): + self.name = name + self.cflags = cflags + self.ldflags = ldflags diff --git a/debuginfo-tests/dexter/dex/dextIR/DebuggerIR.py b/debuginfo-tests/dexter/dex/dextIR/DebuggerIR.py new file mode 100644 index 00000000000..5956db602b4 --- /dev/null +++ b/debuginfo-tests/dexter/dex/dextIR/DebuggerIR.py @@ -0,0 +1,14 @@ +# 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 DebuggerIR: + """Data class which represents a debugger.""" + + def __init__(self, name: str, version: str): + self.name = name + self.version = version diff --git a/debuginfo-tests/dexter/dex/dextIR/DextIR.py b/debuginfo-tests/dexter/dex/dextIR/DextIR.py new file mode 100644 index 00000000000..7638e8b4ab7 --- /dev/null +++ b/debuginfo-tests/dexter/dex/dextIR/DextIR.py @@ -0,0 +1,129 @@ +# 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 collections import OrderedDict +import os +from typing import List + +from dex.dextIR.BuilderIR import BuilderIR +from dex.dextIR.DebuggerIR import DebuggerIR +from dex.dextIR.StepIR import StepIR, StepKind + + +def _step_kind_func(context, step): + if (step.current_location.path is None or + not os.path.exists(step.current_location.path)): + return StepKind.FUNC_UNKNOWN + + if any(os.path.samefile(step.current_location.path, f) + for f in context.options.source_files): + return StepKind.FUNC + + return StepKind.FUNC_EXTERNAL + + +class DextIR: + """A full Dexter test report. + + This is composed of all the other *IR classes. They are used together to + record Dexter inputs and the resultant debugger steps, providing a single + high level access container. + + The Heuristic class works with dexter commands and the generated DextIR to + determine the debugging score for a given test. + + Args: + commands: { name (str), commands (list[CommandIR]) + """ + + def __init__(self, + dexter_version: str, + executable_path: str, + source_paths: List[str], + builder: BuilderIR = None, + debugger: DebuggerIR = None, + commands: OrderedDict = None): + self.dexter_version = dexter_version + self.executable_path = executable_path + self.source_paths = source_paths + self.builder = builder + self.debugger = debugger + self.commands = commands + self.steps: List[StepIR] = [] + + def __str__(self): + colors = 'rgby' + st = '## BEGIN ##\n' + color_idx = 0 + for step in self.steps: + if step.step_kind in (StepKind.FUNC, StepKind.FUNC_EXTERNAL, + StepKind.FUNC_UNKNOWN): + color_idx += 1 + + color = colors[color_idx % len(colors)] + st += '<{}>{}</>\n'.format(color, step) + st += '## END ({} step{}) ##\n'.format( + self.num_steps, '' if self.num_steps == 1 else 's') + return st + + @property + def num_steps(self): + return len(self.steps) + + def _get_prev_step_in_this_frame(self, step): + """Find the most recent step in the same frame as `step`. + + Returns: + StepIR or None if there is no previous step in this frame. + """ + return next((s for s in reversed(self.steps) + if s.current_function == step.current_function + and s.num_frames == step.num_frames), None) + + def _get_new_step_kind(self, context, step): + if step.current_function is None: + return StepKind.UNKNOWN + + if len(self.steps) == 0: + return _step_kind_func(context, step) + + prev_step = self.steps[-1] + + if prev_step.current_function is None: + return StepKind.UNKNOWN + + if prev_step.num_frames < step.num_frames: + return _step_kind_func(context, step) + + if prev_step.num_frames > step.num_frames: + frame_step = self._get_prev_step_in_this_frame(step) + prev_step = frame_step if frame_step is not None else prev_step + + # We're in the same func as prev step, check lineo. + if prev_step.current_location.lineno > step.current_location.lineno: + return StepKind.VERTICAL_BACKWARD + + if prev_step.current_location.lineno < step.current_location.lineno: + return StepKind.VERTICAL_FORWARD + + # We're on the same line as prev step, check column. + if prev_step.current_location.column > step.current_location.column: + return StepKind.HORIZONTAL_BACKWARD + + if prev_step.current_location.column < step.current_location.column: + return StepKind.HORIZONTAL_FORWARD + + # This step is in exactly the same location as the prev step. + return StepKind.SAME + + def new_step(self, context, step): + assert isinstance(step, StepIR), type(step) + step.step_kind = self._get_new_step_kind(context, step) + self.steps.append(step) + return step + + def clear_steps(self): + self.steps.clear() diff --git a/debuginfo-tests/dexter/dex/dextIR/FrameIR.py b/debuginfo-tests/dexter/dex/dextIR/FrameIR.py new file mode 100644 index 00000000000..a2c0523b47b --- /dev/null +++ b/debuginfo-tests/dexter/dex/dextIR/FrameIR.py @@ -0,0 +1,16 @@ +# 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.dextIR.LocIR import LocIR + + +class FrameIR: + """Data class which represents a frame in the call stack""" + + def __init__(self, function: str, is_inlined: bool, loc: LocIR): + self.function = function + self.is_inlined = is_inlined + self.loc = loc diff --git a/debuginfo-tests/dexter/dex/dextIR/LocIR.py b/debuginfo-tests/dexter/dex/dextIR/LocIR.py new file mode 100644 index 00000000000..52a56a8fe80 --- /dev/null +++ b/debuginfo-tests/dexter/dex/dextIR/LocIR.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 +import os + + +class LocIR: + """Data class which represents a source location.""" + + def __init__(self, path: str, lineno: int, column: int): + if path: + path = os.path.normcase(path) + self.path = path + self.lineno = lineno + self.column = column + + def __str__(self): + return '{}({}:{})'.format(self.path, self.lineno, self.column) + + def __eq__(self, rhs): + return (os.path.exists(self.path) and os.path.exists(rhs.path) + and os.path.samefile(self.path, rhs.path) + and self.lineno == rhs.lineno + and self.column == rhs.column) + + def __lt__(self, rhs): + if self.path != rhs.path: + return False + + if self.lineno == rhs.lineno: + return self.column < rhs.column + + return self.lineno < rhs.lineno + + def __gt__(self, rhs): + if self.path != rhs.path: + return False + + if self.lineno == rhs.lineno: + return self.column > rhs.column + + return self.lineno > rhs.lineno diff --git a/debuginfo-tests/dexter/dex/dextIR/ProgramState.py b/debuginfo-tests/dexter/dex/dextIR/ProgramState.py new file mode 100644 index 00000000000..4f05189aed8 --- /dev/null +++ b/debuginfo-tests/dexter/dex/dextIR/ProgramState.py @@ -0,0 +1,117 @@ +# 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 +"""Set of data classes for representing the complete debug program state at a +fixed point in execution. +""" + +import os + +from collections import OrderedDict +from typing import List + +class SourceLocation: + def __init__(self, path: str = None, lineno: int = None, column: int = None): + if path: + path = os.path.normcase(path) + self.path = path + self.lineno = lineno + self.column = column + + def __str__(self): + return '{}({}:{})'.format(self.path, self.lineno, self.column) + + def match(self, other) -> bool: + """Returns true iff all the properties that appear in `self` have the + same value in `other`, but not necessarily vice versa. + """ + if not other or not isinstance(other, SourceLocation): + return False + + if self.path and (self.path != other.path): + return False + + if self.lineno and (self.lineno != other.lineno): + return False + + if self.column and (self.column != other.column): + return False + + return True + + +class StackFrame: + def __init__(self, + function: str = None, + is_inlined: bool = None, + location: SourceLocation = None, + watches: OrderedDict = None): + if watches is None: + watches = {} + + self.function = function + self.is_inlined = is_inlined + self.location = location + self.watches = watches + + def __str__(self): + return '{}{}: {} | {}'.format( + self.function, + ' (inlined)' if self.is_inlined else '', + self.location, + {k: str(self.watches[k]) for k in self.watches}) + + def match(self, other) -> bool: + """Returns true iff all the properties that appear in `self` have the + same value in `other`, but not necessarily vice versa. + """ + if not other or not isinstance(other, StackFrame): + return False + + if self.location and not self.location.match(other.location): + return False + + if self.watches: + for name in iter(self.watches): + try: + if isinstance(self.watches[name], dict): + for attr in iter(self.watches[name]): + if (getattr(other.watches[name], attr, None) != + self.watches[name][attr]): + return False + else: + if other.watches[name].value != self.watches[name]: + return False + except KeyError: + return False + + return True + +class ProgramState: + def __init__(self, frames: List[StackFrame] = None): + self.frames = frames + + def __str__(self): + return '\n'.join(map( + lambda enum: 'Frame {}: {}'.format(enum[0], enum[1]), + enumerate(self.frames))) + + def match(self, other) -> bool: + """Returns true iff all the properties that appear in `self` have the + same value in `other`, but not necessarily vice versa. + """ + if not other or not isinstance(other, ProgramState): + return False + + if self.frames: + for idx, frame in enumerate(self.frames): + try: + if not frame.match(other.frames[idx]): + return False + except (IndexError, KeyError): + return False + + return True diff --git a/debuginfo-tests/dexter/dex/dextIR/StepIR.py b/debuginfo-tests/dexter/dex/dextIR/StepIR.py new file mode 100644 index 00000000000..8111968efe9 --- /dev/null +++ b/debuginfo-tests/dexter/dex/dextIR/StepIR.py @@ -0,0 +1,103 @@ +# 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 +"""Classes which are used to represent debugger steps.""" + +import json + +from collections import OrderedDict +from typing import List +from enum import Enum +from dex.dextIR.FrameIR import FrameIR +from dex.dextIR.LocIR import LocIR +from dex.dextIR.ProgramState import ProgramState + + +class StopReason(Enum): + BREAKPOINT = 0 + STEP = 1 + PROGRAM_EXIT = 2 + ERROR = 3 + OTHER = 4 + + +class StepKind(Enum): + FUNC = 0 + FUNC_EXTERNAL = 1 + FUNC_UNKNOWN = 2 + VERTICAL_FORWARD = 3 + SAME = 4 + VERTICAL_BACKWARD = 5 + UNKNOWN = 6 + HORIZONTAL_FORWARD = 7 + HORIZONTAL_BACKWARD = 8 + + +class StepIR: + """A debugger step. + + Args: + watches (OrderedDict): { expression (str), result (ValueIR) } + """ + + def __init__(self, + step_index: int, + stop_reason: StopReason, + frames: List[FrameIR], + step_kind: StepKind = None, + watches: OrderedDict = None, + program_state: ProgramState = None): + self.step_index = step_index + self.step_kind = step_kind + self.stop_reason = stop_reason + self.program_state = program_state + + if frames is None: + frames = [] + self.frames = frames + + if watches is None: + watches = {} + self.watches = watches + + def __str__(self): + try: + frame = self.current_frame + frame_info = (frame.function, frame.loc.path, frame.loc.lineno, + frame.loc.column) + except AttributeError: + frame_info = (None, None, None, None) + + step_info = (self.step_index, ) + frame_info + ( + str(self.stop_reason), str(self.step_kind), + [w for w in self.watches]) + + return '{}{}'.format('. ' * (self.num_frames - 1), + json.dumps(step_info)) + + @property + def num_frames(self): + return len(self.frames) + + @property + def current_frame(self): + if not len(self.frames): + return None + return self.frames[0] + + @property + def current_function(self): + try: + return self.current_frame.function + except AttributeError: + return None + + @property + def current_location(self): + try: + return self.current_frame.loc + except AttributeError: + return LocIR(path=None, lineno=None, column=None) diff --git a/debuginfo-tests/dexter/dex/dextIR/ValueIR.py b/debuginfo-tests/dexter/dex/dextIR/ValueIR.py new file mode 100644 index 00000000000..9d532acbb21 --- /dev/null +++ b/debuginfo-tests/dexter/dex/dextIR/ValueIR.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 + + +class ValueIR: + """Data class to store the result of an expression evaluation.""" + + def __init__(self, + expression: str, + value: str, + type_name: str, + could_evaluate: bool, + error_string: str = None, + is_optimized_away: bool = False, + is_irretrievable: bool = False): + self.expression = expression + self.value = value + self.type_name = type_name + self.could_evaluate = could_evaluate + self.error_string = error_string + self.is_optimized_away = is_optimized_away + self.is_irretrievable = is_irretrievable + + def __str__(self): + prefix = '"{}": '.format(self.expression) + if self.error_string is not None: + return prefix + self.error_string + if self.value is not None: + return prefix + '({}) {}'.format(self.type_name, self.value) + return (prefix + + 'could_evaluate: {}; irretrievable: {}; optimized_away: {};' + .format(self.could_evaluate, self.is_irretrievable, + self.is_optimized_away)) + diff --git a/debuginfo-tests/dexter/dex/dextIR/__init__.py b/debuginfo-tests/dexter/dex/dextIR/__init__.py new file mode 100644 index 00000000000..463a2c13716 --- /dev/null +++ b/debuginfo-tests/dexter/dex/dextIR/__init__.py @@ -0,0 +1,17 @@ +# 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 +"""dextIR: DExTer Intermediate Representation of DExTer's debugger trace output. +""" + +from dex.dextIR.BuilderIR import BuilderIR +from dex.dextIR.DextIR import DextIR +from dex.dextIR.DebuggerIR import DebuggerIR +from dex.dextIR.FrameIR import FrameIR +from dex.dextIR.LocIR import LocIR +from dex.dextIR.StepIR import StepIR, StepKind, StopReason +from dex.dextIR.ValueIR import ValueIR +from dex.dextIR.ProgramState import ProgramState, SourceLocation, StackFrame diff --git a/debuginfo-tests/dexter/dex/heuristic/Heuristic.py b/debuginfo-tests/dexter/dex/heuristic/Heuristic.py new file mode 100644 index 00000000000..205b767a1ec --- /dev/null +++ b/debuginfo-tests/dexter/dex/heuristic/Heuristic.py @@ -0,0 +1,497 @@ +# 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 +"""Calculate a 'score' based on some dextIR. +Assign penalties based on different commands to decrease the score. +1.000 would be a perfect score. +0.000 is the worst theoretical score possible. +""" + +from collections import defaultdict, namedtuple, Counter +import difflib +import os +from itertools import groupby +from dex.command.StepValueInfo import StepValueInfo + + +PenaltyCommand = namedtuple('PenaltyCommand', ['pen_dict', 'max_penalty']) +# 'meta' field used in different ways by different things +PenaltyInstance = namedtuple('PenaltyInstance', ['meta', 'the_penalty']) + + +def add_heuristic_tool_arguments(parser): + parser.add_argument( + '--penalty-variable-optimized', + type=int, + default=3, + help='set the penalty multiplier for each' + ' occurrence of a variable that was optimized' + ' away', + metavar='<int>') + parser.add_argument( + '--penalty-misordered-values', + type=int, + default=3, + help='set the penalty multiplier for each' + ' occurrence of a misordered value.', + metavar='<int>') + parser.add_argument( + '--penalty-irretrievable', + type=int, + default=4, + help='set the penalty multiplier for each' + " occurrence of a variable that couldn't" + ' be retrieved', + metavar='<int>') + parser.add_argument( + '--penalty-not-evaluatable', + type=int, + default=5, + help='set the penalty multiplier for each' + " occurrence of a variable that couldn't" + ' be evaluated', + metavar='<int>') + parser.add_argument( + '--penalty-missing-values', + type=int, + default=6, + help='set the penalty multiplier for each missing' + ' value', + metavar='<int>') + parser.add_argument( + '--penalty-incorrect-values', + type=int, + default=7, + help='set the penalty multiplier for each' + ' occurrence of an unexpected value.', + metavar='<int>') + parser.add_argument( + '--penalty-unreachable', + type=int, + default=4, # XXX XXX XXX selected by random + help='set the penalty for each line stepped onto that should' + ' have been unreachable.', + metavar='<int>') + parser.add_argument( + '--penalty-misordered-steps', + type=int, + default=2, # XXX XXX XXX selected by random + help='set the penalty for differences in the order of steps' + ' the program was expected to observe.', + metavar='<int>') + parser.add_argument( + '--penalty-missing-step', + type=int, + default=4, # XXX XXX XXX selected by random + help='set the penalty for the program skipping over a step.', + metavar='<int>') + parser.add_argument( + '--penalty-incorrect-program-state', + type=int, + default=4, # XXX XXX XXX selected by random + help='set the penalty for the program never entering an expected state' + ' or entering an unexpected state.', + metavar='<int>') + + +class Heuristic(object): + def __init__(self, context, steps): + self.context = context + self.penalties = {} + + worst_penalty = max([ + self.penalty_variable_optimized, self.penalty_irretrievable, + self.penalty_not_evaluatable, self.penalty_incorrect_values, + self.penalty_missing_values, self.penalty_unreachable, + self.penalty_missing_step, self.penalty_misordered_steps + ]) + + # Get DexExpectWatchType results. + try: + for command in steps.commands['DexExpectWatchType']: + command.eval(steps) + maximum_possible_penalty = min(3, len( + command.values)) * worst_penalty + name, p = self._calculate_expect_watch_penalties( + command, maximum_possible_penalty) + name = name + ' ExpectType' + self.penalties[name] = PenaltyCommand(p, + maximum_possible_penalty) + except KeyError: + pass + + # Get DexExpectWatchValue results. + try: + for command in steps.commands['DexExpectWatchValue']: + command.eval(steps) + maximum_possible_penalty = min(3, len( + command.values)) * worst_penalty + name, p = self._calculate_expect_watch_penalties( + command, maximum_possible_penalty) + name = name + ' ExpectValue' + self.penalties[name] = PenaltyCommand(p, + maximum_possible_penalty) + except KeyError: + pass + + try: + penalties = defaultdict(list) + maximum_possible_penalty_all = 0 + for expect_state in steps.commands['DexExpectProgramState']: + success = expect_state.eval(steps) + p = 0 if success else self.penalty_incorrect_program_state + + meta = 'expected {}: {}'.format( + '{} times'.format(expect_state.times) + if expect_state.times >= 0 else 'at least once', + expect_state.program_state_text) + + if success: + meta = '<g>{}</>'.format(meta) + + maximum_possible_penalty = self.penalty_incorrect_program_state + maximum_possible_penalty_all += maximum_possible_penalty + name = expect_state.program_state_text + penalties[meta] = [PenaltyInstance('{} times'.format( + len(expect_state.encounters)), p)] + self.penalties['expected program states'] = PenaltyCommand( + penalties, maximum_possible_penalty_all) + except KeyError: + pass + + # Get the total number of each step kind. + step_kind_counts = defaultdict(int) + for step in getattr(steps, 'steps'): + step_kind_counts[step.step_kind] += 1 + + # Get DexExpectStepKind results. + penalties = defaultdict(list) + maximum_possible_penalty_all = 0 + try: + for command in steps.commands['DexExpectStepKind']: + command.eval() + # Cap the penalty at 2 * expected count or else 1 + maximum_possible_penalty = max(command.count * 2, 1) + p = abs(command.count - step_kind_counts[command.name]) + actual_penalty = min(p, maximum_possible_penalty) + key = ('{}'.format(command.name) + if actual_penalty else '<g>{}</>'.format(command.name)) + penalties[key] = [PenaltyInstance(p, actual_penalty)] + maximum_possible_penalty_all += maximum_possible_penalty + self.penalties['step kind differences'] = PenaltyCommand( + penalties, maximum_possible_penalty_all) + except KeyError: + pass + + if 'DexUnreachable' in steps.commands: + cmds = steps.commands['DexUnreachable'] + unreach_count = 0 + + # Find steps with unreachable in them + ureachs = [ + s for s in steps.steps if 'DexUnreachable' in s.watches.keys() + ] + + # There's no need to match up cmds with the actual watches + upen = self.penalty_unreachable + + count = upen * len(ureachs) + if count != 0: + d = dict() + for x in ureachs: + msg = 'line {} reached'.format(x.current_location.lineno) + d[msg] = [PenaltyInstance(upen, upen)] + else: + d = { + '<g>No unreachable lines seen</>': [PenaltyInstance(0, 0)] + } + total = PenaltyCommand(d, len(cmds) * upen) + + self.penalties['unreachable lines'] = total + + if 'DexExpectStepOrder' in steps.commands: + cmds = steps.commands['DexExpectStepOrder'] + + # Form a list of which line/cmd we _should_ have seen + cmd_num_lst = [(x, c.lineno) for c in cmds + for x in c.sequence] + # Order them by the sequence number + cmd_num_lst.sort(key=lambda t: t[0]) + # Strip out sequence key + cmd_num_lst = [y for x, y in cmd_num_lst] + + # Now do the same, but for the actually observed lines/cmds + ss = steps.steps + deso = [s for s in ss if 'DexExpectStepOrder' in s.watches.keys()] + deso = [s.watches['DexExpectStepOrder'] for s in deso] + # We rely on the steps remaining in order here + order_list = [int(x.expression) for x in deso] + + # First off, check to see whether or not there are missing items + expected = Counter(cmd_num_lst) + seen = Counter(order_list) + + unseen_line_dict = dict() + skipped_line_dict = dict() + + mispen = self.penalty_missing_step + num_missing = 0 + num_repeats = 0 + for k, v in expected.items(): + if k not in seen: + msg = 'Line {} not seen'.format(k) + unseen_line_dict[msg] = [PenaltyInstance(mispen, mispen)] + num_missing += v + elif v > seen[k]: + msg = 'Line {} skipped at least once'.format(k) + skipped_line_dict[msg] = [PenaltyInstance(mispen, mispen)] + num_missing += v - seen[k] + elif v < seen[k]: + # Don't penalise unexpected extra sightings of a line + # for now + num_repeats = seen[k] - v + pass + + if len(unseen_line_dict) == 0: + pi = PenaltyInstance(0, 0) + unseen_line_dict['<g>All lines were seen</>'] = [pi] + + if len(skipped_line_dict) == 0: + pi = PenaltyInstance(0, 0) + skipped_line_dict['<g>No lines were skipped</>'] = [pi] + + total = PenaltyCommand(unseen_line_dict, len(expected) * mispen) + self.penalties['Unseen lines'] = total + total = PenaltyCommand(skipped_line_dict, len(expected) * mispen) + self.penalties['Skipped lines'] = total + + ordpen = self.penalty_misordered_steps + cmd_num_lst = [str(x) for x in cmd_num_lst] + order_list = [str(x) for x in order_list] + lst = list(difflib.Differ().compare(cmd_num_lst, order_list)) + diff_detail = Counter(l[0] for l in lst) + + assert '?' not in diff_detail + + # Diffs are hard to interpret; there are many algorithms for + # condensing them. Ignore all that, and just print out the changed + # sequences, it's up to the user to interpret what's going on. + + def filt_lines(s, seg, e, key): + lst = [s] + for x in seg: + if x[0] == key: + lst.append(int(x[2:])) + lst.append(e) + return lst + + diff_msgs = dict() + + def reportdiff(start_idx, segment, end_idx): + msg = 'Order mismatch, expected linenos {}, saw {}' + expected_linenos = filt_lines(start_idx, segment, end_idx, '-') + seen_linenos = filt_lines(start_idx, segment, end_idx, '+') + msg = msg.format(expected_linenos, seen_linenos) + diff_msgs[msg] = [PenaltyInstance(ordpen, ordpen)] + + # Group by changed segments. + start_expt_step = 0 + end_expt_step = 0 + to_print_lst = [] + for k, subit in groupby(lst, lambda x: x[0] == ' '): + if k: # Whitespace group + nochanged = [x for x in subit] + end_expt_step = int(nochanged[0][2:]) + if len(to_print_lst) > 0: + reportdiff(start_expt_step, to_print_lst, + end_expt_step) + start_expt_step = int(nochanged[-1][2:]) + to_print_lst = [] + else: # Diff group, save for printing + to_print_lst = [x for x in subit] + + # If there was a dangling different step, print that too. + if len(to_print_lst) > 0: + reportdiff(start_expt_step, to_print_lst, '[End]') + + if len(diff_msgs) == 0: + diff_msgs['<g>No lines misordered</>'] = [ + PenaltyInstance(0, 0) + ] + total = PenaltyCommand(diff_msgs, len(cmd_num_lst) * ordpen) + self.penalties['Misordered lines'] = total + + return + + def _calculate_expect_watch_penalties(self, c, maximum_possible_penalty): + penalties = defaultdict(list) + + if c.line_range[0] == c.line_range[-1]: + line_range = str(c.line_range[0]) + else: + line_range = '{}-{}'.format(c.line_range[0], c.line_range[-1]) + + name = '{}:{} [{}]'.format( + os.path.basename(c.path), line_range, c.expression) + + num_actual_watches = len(c.expected_watches) + len( + c.unexpected_watches) + + penalty_available = maximum_possible_penalty + + # Only penalize for missing values if we have actually seen a watch + # that's returned us an actual value at some point, or if we've not + # encountered the value at all. + if num_actual_watches or c.times_encountered == 0: + for v in c.missing_values: + current_penalty = min(penalty_available, + self.penalty_missing_values) + penalty_available -= current_penalty + penalties['missing values'].append( + PenaltyInstance(v, current_penalty)) + + for v in c.encountered_values: + penalties['<g>expected encountered watches</>'].append( + PenaltyInstance(v, 0)) + + penalty_descriptions = [ + (self.penalty_not_evaluatable, c.invalid_watches, + 'could not evaluate'), + (self.penalty_variable_optimized, c.optimized_out_watches, + 'result optimized away'), + (self.penalty_misordered_values, c.misordered_watches, + 'misordered result'), + (self.penalty_irretrievable, c.irretrievable_watches, + 'result could not be retrieved'), + (self.penalty_incorrect_values, c.unexpected_watches, + 'unexpected result'), + ] + + for penalty_score, watches, description in penalty_descriptions: + # We only penalize the encountered issue for each missing value per + # command but we still want to record each one, so set the penalty + # to 0 after the threshold is passed. + times_to_penalize = len(c.missing_values) + + for w in watches: + times_to_penalize -= 1 + penalty_score = min(penalty_available, penalty_score) + penalty_available -= penalty_score + penalties[description].append( + PenaltyInstance(w, penalty_score)) + if not times_to_penalize: + penalty_score = 0 + + return name, penalties + + @property + def penalty(self): + result = 0 + + maximum_allowed_penalty = 0 + for name, pen_cmd in self.penalties.items(): + maximum_allowed_penalty += pen_cmd.max_penalty + value = pen_cmd.pen_dict + for category, inst_list in value.items(): + result += sum(x.the_penalty for x in inst_list) + return min(result, maximum_allowed_penalty) + + @property + def max_penalty(self): + return sum(p_cat.max_penalty for p_cat in self.penalties.values()) + + @property + def score(self): + try: + return 1.0 - (self.penalty / float(self.max_penalty)) + except ZeroDivisionError: + return float('nan') + + @property + def summary_string(self): + score = self.score + isnan = score != score # pylint: disable=comparison-with-itself + color = 'g' + if score < 0.25 or isnan: + color = 'r' + elif score < 0.75: + color = 'y' + + return '<{}>({:.4f})</>'.format(color, score) + + @property + def verbose_output(self): # noqa + string = '' + string += ('\n') + for command in sorted(self.penalties): + pen_cmd = self.penalties[command] + maximum_possible_penalty = pen_cmd.max_penalty + total_penalty = 0 + lines = [] + for category in sorted(pen_cmd.pen_dict): + lines.append(' <r>{}</>:\n'.format(category)) + + for result, penalty in pen_cmd.pen_dict[category]: + if isinstance(result, StepValueInfo): + text = 'step {}'.format(result.step_index) + if result.expected_value: + text += ' ({})'.format(result.expected_value) + else: + text = str(result) + if penalty: + assert penalty > 0, penalty + total_penalty += penalty + text += ' <r>[-{}]</>'.format(penalty) + lines.append(' {}\n'.format(text)) + + lines.append('\n') + + string += (' <b>{}</> <y>[{}/{}]</>\n'.format( + command, total_penalty, maximum_possible_penalty)) + for line in lines: + string += (line) + string += ('\n') + return string + + @property + def penalty_variable_optimized(self): + return self.context.options.penalty_variable_optimized + + @property + def penalty_irretrievable(self): + return self.context.options.penalty_irretrievable + + @property + def penalty_not_evaluatable(self): + return self.context.options.penalty_not_evaluatable + + @property + def penalty_incorrect_values(self): + return self.context.options.penalty_incorrect_values + + @property + def penalty_missing_values(self): + return self.context.options.penalty_missing_values + + @property + def penalty_misordered_values(self): + return self.context.options.penalty_misordered_values + + @property + def penalty_unreachable(self): + return self.context.options.penalty_unreachable + + @property + def penalty_missing_step(self): + return self.context.options.penalty_missing_step + + @property + def penalty_misordered_steps(self): + return self.context.options.penalty_misordered_steps + + @property + def penalty_incorrect_program_state(self): + return self.context.options.penalty_incorrect_program_state diff --git a/debuginfo-tests/dexter/dex/heuristic/__init__.py b/debuginfo-tests/dexter/dex/heuristic/__init__.py new file mode 100644 index 00000000000..2a143f6c72b --- /dev/null +++ b/debuginfo-tests/dexter/dex/heuristic/__init__.py @@ -0,0 +1,8 @@ +# 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.heuristic.Heuristic import Heuristic, StepValueInfo diff --git a/debuginfo-tests/dexter/dex/tools/Main.py b/debuginfo-tests/dexter/dex/tools/Main.py new file mode 100644 index 00000000000..78fb4f7b724 --- /dev/null +++ b/debuginfo-tests/dexter/dex/tools/Main.py @@ -0,0 +1,207 @@ +# 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 +"""This is the main entry point. +It implements some functionality common to all subtools such as command line +parsing and running the unit-testing harnesses, before calling the reequested +subtool. +""" + +import imp +import os +import sys + +from dex.utils import PrettyOutput, Timer +from dex.utils import ExtArgParse as argparse +from dex.utils import get_root_directory +from dex.utils.Exceptions import Error, ToolArgumentError +from dex.utils.UnitTests import unit_tests_ok +from dex.utils.Version import version +from dex.utils import WorkingDirectory +from dex.utils.ReturnCode import ReturnCode + + +def _output_bug_report_message(context): + """ In the event of a catastrophic failure, print bug report request to the + user. + """ + context.o.red( + '\n\n' + '<g>****************************************</>\n' + '<b>****************************************</>\n' + '****************************************\n' + '** **\n' + '** <y>This is a bug in <a>DExTer</>.</> **\n' + '** **\n' + '** <y>Please report it.</> **\n' + '** **\n' + '****************************************\n' + '<b>****************************************</>\n' + '<g>****************************************</>\n' + '\n' + '<b>system:</>\n' + '<d>{}</>\n\n' + '<b>version:</>\n' + '<d>{}</>\n\n' + '<b>args:</>\n' + '<d>{}</>\n' + '\n'.format(sys.platform, version('DExTer'), + [sys.executable] + sys.argv), + stream=PrettyOutput.stderr) + + +def get_tools_directory(): + """ Returns directory path where DExTer tool imports can be + found. + """ + tools_directory = os.path.join(get_root_directory(), 'tools') + assert os.path.isdir(tools_directory), tools_directory + return tools_directory + + +def get_tool_names(): + """ Returns a list of expected DExTer Tools + """ + return [ + 'clang-opt-bisect', 'help', 'list-debuggers', 'no-tool-', + 'run-debugger-internal-', 'test', 'view' + ] + + +def _set_auto_highlights(context): + """Flag some strings for auto-highlighting. + """ + context.o.auto_reds.extend([ + r'[Ee]rror\:', + r'[Ee]xception\:', + r'un(expected|recognized) argument', + ]) + context.o.auto_yellows.extend([ + r'[Ww]arning\:', + r'\(did you mean ', + r'During handling of the above exception, another exception', + ]) + + +def _get_options_and_args(context): + """ get the options and arguments from the commandline + """ + parser = argparse.ExtArgumentParser(context, add_help=False) + parser.add_argument('tool', default=None, nargs='?') + options, args = parser.parse_known_args(sys.argv[1:]) + + return options, args + + +def _get_tool_name(options): + """ get the name of the dexter tool (if passed) specified on the command + line, otherwise return 'no_tool_'. + """ + tool_name = options.tool + if tool_name is None: + tool_name = 'no_tool_' + else: + _is_valid_tool_name(tool_name) + return tool_name + + +def _is_valid_tool_name(tool_name): + """ check tool name matches a tool directory within the dexter tools + directory. + """ + valid_tools = get_tool_names() + if tool_name not in valid_tools: + raise Error('invalid tool "{}" (choose from {})'.format( + tool_name, + ', '.join([t for t in valid_tools if not t.endswith('-')]))) + + +def _import_tool_module(tool_name): + """ Imports the python module at the tool directory specificed by + tool_name. + """ + # format tool argument to reflect tool directory form. + tool_name = tool_name.replace('-', '_') + + tools_directory = get_tools_directory() + module_info = imp.find_module(tool_name, [tools_directory]) + + return imp.load_module(tool_name, *module_info) + + +def tool_main(context, tool, args): + with Timer(tool.name): + options, defaults = tool.parse_command_line(args) + Timer.display = options.time_report + Timer.indent = options.indent_timer_level + Timer.fn = context.o.blue + context.options = options + context.version = version(tool.name) + + if options.version: + context.o.green('{}\n'.format(context.version)) + return ReturnCode.OK + + if (options.unittest != 'off' and not unit_tests_ok(context)): + raise Error('<d>unit test failures</>') + + if options.colortest: + context.o.colortest() + return ReturnCode.OK + + try: + tool.handle_base_options(defaults) + except ToolArgumentError as e: + raise Error(e) + + dir_ = context.options.working_directory + with WorkingDirectory(context, dir=dir_) as context.working_directory: + return_code = tool.go() + + return return_code + + +class Context(object): + """Context encapsulates globally useful objects and data; passed to many + Dexter functions. + """ + + def __init__(self): + self.o: PrettyOutput = None + self.working_directory: str = None + self.options: dict = None + self.version: str = None + self.root_directory: str = None + + +def main() -> ReturnCode: + + context = Context() + + with PrettyOutput() as context.o: + try: + context.root_directory = get_root_directory() + # Flag some strings for auto-highlighting. + _set_auto_highlights(context) + options, args = _get_options_and_args(context) + # raises 'Error' if command line tool is invalid. + tool_name = _get_tool_name(options) + module = _import_tool_module(tool_name) + return tool_main(context, module.Tool(context), args) + except Error as e: + context.o.auto( + '\nerror: {}\n'.format(str(e)), stream=PrettyOutput.stderr) + try: + if context.options.error_debug: + raise + except AttributeError: + pass + return ReturnCode._ERROR + except (KeyboardInterrupt, SystemExit): + raise + except: # noqa + _output_bug_report_message(context) + raise diff --git a/debuginfo-tests/dexter/dex/tools/TestToolBase.py b/debuginfo-tests/dexter/dex/tools/TestToolBase.py new file mode 100644 index 00000000000..7e00fc54b19 --- /dev/null +++ b/debuginfo-tests/dexter/dex/tools/TestToolBase.py @@ -0,0 +1,148 @@ +# 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 subtools that do build/run tests.""" + +import abc +from datetime import datetime +import os +import sys + +from dex.builder import add_builder_tool_arguments +from dex.builder import handle_builder_tool_options +from dex.debugger.Debuggers import add_debugger_tool_arguments +from dex.debugger.Debuggers import handle_debugger_tool_options +from dex.heuristic.Heuristic import add_heuristic_tool_arguments +from dex.tools.ToolBase import ToolBase +from dex.utils import get_root_directory, warn +from dex.utils.Exceptions import Error, ToolArgumentError +from dex.utils.ReturnCode import ReturnCode + + +class TestToolBase(ToolBase): + def __init__(self, *args, **kwargs): + super(TestToolBase, self).__init__(*args, **kwargs) + self.build_script: str = None + + def add_tool_arguments(self, parser, defaults): + parser.description = self.__doc__ + add_builder_tool_arguments(parser) + add_debugger_tool_arguments(parser, self.context, defaults) + add_heuristic_tool_arguments(parser) + + parser.add_argument( + 'test_path', + type=str, + metavar='<test-path>', + nargs='?', + default=os.path.abspath( + os.path.join(get_root_directory(), '..', 'tests')), + help='directory containing test(s)') + + parser.add_argument( + '--results-directory', + type=str, + metavar='<directory>', + default=os.path.abspath( + os.path.join(get_root_directory(), '..', 'results', + datetime.now().strftime('%Y-%m-%d-%H%M-%S'))), + help='directory to save results') + + def handle_options(self, defaults): + options = self.context.options + + # We accept either or both of --binary and --builder. + if not options.binary and not options.builder: + raise Error('expected --builder or --binary') + + # --binary overrides --builder + if options.binary: + if options.builder: + warn(self.context, "overriding --builder with --binary\n") + + options.binary = os.path.abspath(options.binary) + if not os.path.isfile(options.binary): + raise Error('<d>could not find binary file</> <r>"{}"</>' + .format(options.binary)) + else: + try: + self.build_script = handle_builder_tool_options(self.context) + except ToolArgumentError as e: + raise Error(e) + + try: + handle_debugger_tool_options(self.context, defaults) + except ToolArgumentError as e: + raise Error(e) + + options.test_path = os.path.abspath(options.test_path) + if not os.path.isfile(options.test_path) and not os.path.isdir(options.test_path): + raise Error( + '<d>could not find test path</> <r>"{}"</>'.format( + options.test_path)) + + options.results_directory = os.path.abspath(options.results_directory) + if not os.path.isdir(options.results_directory): + try: + os.makedirs(options.results_directory, exist_ok=True) + except OSError as e: + raise Error( + '<d>could not create directory</> <r>"{}"</> <y>({})</>'. + format(options.results_directory, e.strerror)) + + def go(self) -> ReturnCode: # noqa + options = self.context.options + + options.executable = os.path.join( + self.context.working_directory.path, 'tmp.exe') + + if os.path.isdir(options.test_path): + + subdirs = sorted([ + r for r, _, f in os.walk(options.test_path) + if 'test.cfg' in f + ]) + + for subdir in subdirs: + + # TODO: read file extensions from the test.cfg file instead so + # that this isn't just limited to C and C++. + options.source_files = [ + os.path.normcase(os.path.join(subdir, f)) + for f in os.listdir(subdir) if any( + f.endswith(ext) for ext in ['.c', '.cpp']) + ] + + self._run_test(self._get_test_name(subdir)) + else: + options.source_files = [options.test_path] + self._run_test(self._get_test_name(options.test_path)) + + return self._handle_results() + + @staticmethod + def _is_current_directory(test_directory): + return test_directory == '.' + + def _get_test_name(self, test_path): + """Get the test name from either the test file, or the sub directory + path it's stored in. + """ + # test names are distinguished by their relative path from the + # specified test path. + test_name = os.path.relpath(test_path, + self.context.options.test_path) + if self._is_current_directory(test_name): + test_name = os.path.basename(test_path) + return test_name + + @abc.abstractmethod + def _run_test(self, test_dir): + pass + + @abc.abstractmethod + def _handle_results(self) -> ReturnCode: + pass diff --git a/debuginfo-tests/dexter/dex/tools/ToolBase.py b/debuginfo-tests/dexter/dex/tools/ToolBase.py new file mode 100644 index 00000000000..eb6ba94c2de --- /dev/null +++ b/debuginfo-tests/dexter/dex/tools/ToolBase.py @@ -0,0 +1,135 @@ +# 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 subtools.""" + +import abc +import os +import tempfile + +from dex import __version__ +from dex.utils import ExtArgParse +from dex.utils import PrettyOutput +from dex.utils.ReturnCode import ReturnCode + + +class ToolBase(object, metaclass=abc.ABCMeta): + def __init__(self, context): + self.context = context + self.parser = None + + @abc.abstractproperty + def name(self): + pass + + @abc.abstractmethod + def add_tool_arguments(self, parser, defaults): + pass + + def parse_command_line(self, args): + """Define two parsers: pparser and self.parser. + pparser deals with args that need to be parsed prior to any of those of + self.parser. For example, any args which may affect the state of + argparse error output. + """ + + class defaults(object): + pass + + pparser = ExtArgParse.ExtArgumentParser( + self.context, add_help=False, prog=self.name) + + pparser.add_argument( + '--no-color-output', + action='store_true', + default=False, + help='do not use colored output on stdout/stderr') + pparser.add_argument( + '--time-report', + action='store_true', + default=False, + help='display timing statistics') + + self.parser = ExtArgParse.ExtArgumentParser( + self.context, parents=[pparser], prog=self.name) + self.parser.add_argument( + '-v', + '--verbose', + action='store_true', + default=False, + help='enable verbose output') + self.parser.add_argument( + '-V', + '--version', + action='store_true', + default=False, + help='display the DExTer version and exit') + self.parser.add_argument( + '-w', + '--no-warnings', + action='store_true', + default=False, + help='suppress warning output') + self.parser.add_argument( + '--unittest', + type=str, + choices=['off', 'show-failures', 'show-all'], + default='off', + help='run the DExTer codebase unit tests') + + suppress = ExtArgParse.SUPPRESS # pylint: disable=no-member + self.parser.add_argument( + '--colortest', action='store_true', default=False, help=suppress) + self.parser.add_argument( + '--error-debug', action='store_true', default=False, help=suppress) + defaults.working_directory = os.path.join(tempfile.gettempdir(), + 'dexter') + self.parser.add_argument( + '--indent-timer-level', type=int, default=1, help=suppress) + self.parser.add_argument( + '--working-directory', + type=str, + metavar='<file>', + default=None, + display_default=defaults.working_directory, + help='location of working directory') + self.parser.add_argument( + '--save-temps', + action='store_true', + default=False, + help='save temporary files') + + self.add_tool_arguments(self.parser, defaults) + + # If an error is encountered during pparser, show the full usage text + # including self.parser options. Strip the preceding 'usage: ' to avoid + # having it appear twice. + pparser.usage = self.parser.format_usage().lstrip('usage: ') + + options, args = pparser.parse_known_args(args) + + if options.no_color_output: + PrettyOutput.stdout.color_enabled = False + PrettyOutput.stderr.color_enabled = False + + options = self.parser.parse_args(args, namespace=options) + return options, defaults + + def handle_base_options(self, defaults): + self.handle_options(defaults) + + options = self.context.options + + if options.working_directory is None: + options.working_directory = defaults.working_directory + + @abc.abstractmethod + def handle_options(self, defaults): + pass + + @abc.abstractmethod + def go(self) -> ReturnCode: + pass diff --git a/debuginfo-tests/dexter/dex/tools/__init__.py b/debuginfo-tests/dexter/dex/tools/__init__.py new file mode 100644 index 00000000000..76d12614b07 --- /dev/null +++ b/debuginfo-tests/dexter/dex/tools/__init__.py @@ -0,0 +1,10 @@ +# 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.tools.Main import Context, get_tool_names, get_tools_directory, main, tool_main +from dex.tools.TestToolBase import TestToolBase +from dex.tools.ToolBase import ToolBase diff --git a/debuginfo-tests/dexter/dex/tools/clang_opt_bisect/Tool.py b/debuginfo-tests/dexter/dex/tools/clang_opt_bisect/Tool.py new file mode 100644 index 00000000000..a2fc2969e96 --- /dev/null +++ b/debuginfo-tests/dexter/dex/tools/clang_opt_bisect/Tool.py @@ -0,0 +1,286 @@ +# 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 +"""Clang opt-bisect tool.""" + +from collections import defaultdict +import os +import csv +import re +import pickle + +from dex.builder import run_external_build_script +from dex.debugger.Debuggers import empty_debugger_steps, get_debugger_steps +from dex.heuristic import Heuristic +from dex.tools import TestToolBase +from dex.utils.Exceptions import DebuggerException, Error +from dex.utils.Exceptions import BuildScriptException, HeuristicException +from dex.utils.PrettyOutputBase import Stream +from dex.utils.ReturnCode import ReturnCode + + +class BisectPass(object): + def __init__(self, no, description, description_no_loc): + self.no = no + self.description = description + self.description_no_loc = description_no_loc + + self.penalty = 0 + self.differences = [] + + +class Tool(TestToolBase): + """Use the LLVM "-opt-bisect-limit=<n>" flag to get information on the + contribution of each LLVM pass to the overall DExTer score when using + clang. + + Clang is run multiple times, with an increasing value of n, measuring the + debugging experience at each value. + """ + + _re_running_pass = re.compile( + r'^BISECT\: running pass \((\d+)\) (.+?)( \(.+\))?$') + + def __init__(self, *args, **kwargs): + super(Tool, self).__init__(*args, **kwargs) + self._all_bisect_pass_summary = defaultdict(list) + + @property + def name(self): + return 'DExTer clang opt bisect' + + def _get_bisect_limits(self): + options = self.context.options + + max_limit = 999999 + limits = [max_limit for _ in options.source_files] + all_passes = [ + l for l in self._clang_opt_bisect_build(limits)[1].splitlines() + if l.startswith('BISECT: running pass (') + ] + + results = [] + for i, pass_ in enumerate(all_passes[1:]): + if pass_.startswith('BISECT: running pass (1)'): + results.append(all_passes[i]) + results.append(all_passes[-1]) + + assert len(results) == len( + options.source_files), (results, options.source_files) + + limits = [ + int(Tool._re_running_pass.match(r).group(1)) for r in results + ] + + return limits + + def _run_test(self, test_name): # noqa + options = self.context.options + + per_pass_score = [] + current_bisect_pass_summary = defaultdict(list) + + max_limits = self._get_bisect_limits() + overall_limit = sum(max_limits) + prev_score = 1.0 + prev_steps_str = None + + for current_limit in range(overall_limit + 1): + # Take the overall limit number and split it across buckets for + # each source file. + limit_remaining = current_limit + file_limits = [0] * len(max_limits) + for i, max_limit in enumerate(max_limits): + if limit_remaining < max_limit: + file_limits[i] += limit_remaining + break + else: + file_limits[i] = max_limit + limit_remaining -= file_limits[i] + + f = [l for l in file_limits if l] + current_file_index = len(f) - 1 if f else 0 + + _, err, builderIR = self._clang_opt_bisect_build(file_limits) + err_lines = err.splitlines() + # Find the last line that specified a running pass. + for l in err_lines[::-1]: + match = Tool._re_running_pass.match(l) + if match: + pass_info = match.groups() + break + else: + pass_info = (0, None, None) + + try: + steps = get_debugger_steps(self.context) + except DebuggerException: + steps = empty_debugger_steps(self.context) + + steps.builder = builderIR + + try: + heuristic = Heuristic(self.context, steps) + except HeuristicException as e: + raise Error(e) + + score_difference = heuristic.score - prev_score + prev_score = heuristic.score + + isnan = heuristic.score != heuristic.score + if isnan or score_difference < 0: + color1 = 'r' + color2 = 'r' + elif score_difference > 0: + color1 = 'g' + color2 = 'g' + else: + color1 = 'y' + color2 = 'd' + + summary = '<{}>running pass {}/{} on "{}"'.format( + color2, pass_info[0], max_limits[current_file_index], + test_name) + if len(options.source_files) > 1: + summary += ' [{}/{}]'.format(current_limit, overall_limit) + + pass_text = ''.join(p for p in pass_info[1:] if p) + summary += ': {} <{}>{:+.4f}</> <{}>{}</></>\n'.format( + heuristic.summary_string, color1, score_difference, color2, + pass_text) + + self.context.o.auto(summary) + + heuristic_verbose_output = heuristic.verbose_output + + if options.verbose: + self.context.o.auto(heuristic_verbose_output) + + steps_str = str(steps) + steps_changed = steps_str != prev_steps_str + prev_steps_str = steps_str + + # If this is the first pass, or something has changed, write a text + # file containing verbose information on the current status. + if current_limit == 0 or score_difference or steps_changed: + file_name = '-'.join( + str(s) for s in [ + 'status', test_name, '{{:0>{}}}'.format( + len(str(overall_limit))).format(current_limit), + '{:.4f}'.format(heuristic.score).replace( + '.', '_'), pass_info[1] + ] if s is not None) + + file_name = ''.join( + c for c in file_name + if c.isalnum() or c in '()-_./ ').strip().replace( + ' ', '_').replace('/', '_') + + output_text_path = os.path.join(options.results_directory, + '{}.txt'.format(file_name)) + with open(output_text_path, 'w') as fp: + self.context.o.auto(summary + '\n', stream=Stream(fp)) + self.context.o.auto(str(steps) + '\n', stream=Stream(fp)) + self.context.o.auto( + heuristic_verbose_output + '\n', stream=Stream(fp)) + + output_dextIR_path = os.path.join(options.results_directory, + '{}.dextIR'.format(file_name)) + with open(output_dextIR_path, 'wb') as fp: + pickle.dump(steps, fp, protocol=pickle.HIGHEST_PROTOCOL) + + per_pass_score.append((test_name, pass_text, + heuristic.score)) + + if pass_info[1]: + self._all_bisect_pass_summary[pass_info[1]].append( + score_difference) + + current_bisect_pass_summary[pass_info[1]].append( + score_difference) + + per_pass_score_path = os.path.join( + options.results_directory, + '{}-per_pass_score.csv'.format(test_name)) + + with open(per_pass_score_path, mode='w', newline='') as fp: + writer = csv.writer(fp, delimiter=',') + writer.writerow(['Source File', 'Pass', 'Score']) + + for path, pass_, score in per_pass_score: + writer.writerow([path, pass_, score]) + self.context.o.blue('wrote "{}"\n'.format(per_pass_score_path)) + + pass_summary_path = os.path.join( + options.results_directory, '{}-pass-summary.csv'.format(test_name)) + + self._write_pass_summary(pass_summary_path, + current_bisect_pass_summary) + + def _handle_results(self) -> ReturnCode: + options = self.context.options + pass_summary_path = os.path.join(options.results_directory, + 'overall-pass-summary.csv') + + self._write_pass_summary(pass_summary_path, + self._all_bisect_pass_summary) + return ReturnCode.OK + + + def _clang_opt_bisect_build(self, opt_bisect_limits): + options = self.context.options + compiler_options = [ + '{} -mllvm -opt-bisect-limit={}'.format(options.cflags, + opt_bisect_limit) + for opt_bisect_limit in opt_bisect_limits + ] + linker_options = options.ldflags + + try: + return run_external_build_script( + self.context, + source_files=options.source_files, + compiler_options=compiler_options, + linker_options=linker_options, + script_path=self.build_script, + executable_file=options.executable) + except BuildScriptException as e: + raise Error(e) + + def _write_pass_summary(self, path, pass_summary): + # Get a list of tuples. + pass_summary_list = list(pass_summary.items()) + + for i, item in enumerate(pass_summary_list): + # Add elems for the sum, min, and max of the values, as well as + # 'interestingness' which is whether any of these values are + # non-zero. + pass_summary_list[i] += (sum(item[1]), min(item[1]), max(item[1]), + any(item[1])) + + # Split the pass name into the basic name and kind. + pass_summary_list[i] += tuple(item[0].rsplit(' on ', 1)) + + # Sort the list by the following columns in order of precedence: + # - Is interesting (True first) + # - Sum (smallest first) + # - Number of times pass ran (largest first) + # - Kind (alphabetically) + # - Name (alphabetically) + pass_summary_list.sort( + key=lambda tup: (not tup[5], tup[2], -len(tup[1]), tup[7], tup[6])) + + with open(path, mode='w', newline='') as fp: + writer = csv.writer(fp, delimiter=',') + writer.writerow( + ['Pass', 'Kind', 'Sum', 'Min', 'Max', 'Interesting']) + + for (_, vals, sum_, min_, max_, interesting, name, + kind) in pass_summary_list: + writer.writerow([name, kind, sum_, min_, max_, interesting] + + vals) + + self.context.o.blue('wrote "{}"\n'.format(path)) diff --git a/debuginfo-tests/dexter/dex/tools/clang_opt_bisect/__init__.py b/debuginfo-tests/dexter/dex/tools/clang_opt_bisect/__init__.py new file mode 100644 index 00000000000..b933e690b23 --- /dev/null +++ b/debuginfo-tests/dexter/dex/tools/clang_opt_bisect/__init__.py @@ -0,0 +1,8 @@ +# 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.tools.clang_opt_bisect.Tool import Tool diff --git a/debuginfo-tests/dexter/dex/tools/help/Tool.py b/debuginfo-tests/dexter/dex/tools/help/Tool.py new file mode 100644 index 00000000000..2b35af4b98f --- /dev/null +++ b/debuginfo-tests/dexter/dex/tools/help/Tool.py @@ -0,0 +1,61 @@ +# 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 +"""Help tool.""" + +import imp +import textwrap + +from dex.tools import ToolBase, get_tool_names, get_tools_directory, tool_main +from dex.utils.ReturnCode import ReturnCode + + +class Tool(ToolBase): + """Provides help info on subtools.""" + + @property + def name(self): + return 'DExTer help' + + @property + def _visible_tool_names(self): + return [t for t in get_tool_names() if not t.endswith('-')] + + def add_tool_arguments(self, parser, defaults): + parser.description = Tool.__doc__ + parser.add_argument( + 'tool', + choices=self._visible_tool_names, + nargs='?', + help='name of subtool') + + def handle_options(self, defaults): + pass + + @property + def _default_text(self): + s = '\n<b>The following subtools are available:</>\n\n' + tools_directory = get_tools_directory() + for tool_name in sorted(self._visible_tool_names): + internal_name = tool_name.replace('-', '_') + module_info = imp.find_module(internal_name, [tools_directory]) + tool_doc = imp.load_module(internal_name, + *module_info).Tool.__doc__ + tool_doc = tool_doc.strip() if tool_doc else '' + tool_doc = textwrap.fill(' '.join(tool_doc.split()), 80) + s += '<g>{}</>\n{}\n\n'.format(tool_name, tool_doc) + return s + + def go(self) -> ReturnCode: + if self.context.options.tool is None: + self.context.o.auto(self._default_text) + return ReturnCode.OK + + tool_name = self.context.options.tool.replace('-', '_') + tools_directory = get_tools_directory() + module_info = imp.find_module(tool_name, [tools_directory]) + module = imp.load_module(tool_name, *module_info) + return tool_main(self.context, module.Tool(self.context), ['--help']) diff --git a/debuginfo-tests/dexter/dex/tools/help/__init__.py b/debuginfo-tests/dexter/dex/tools/help/__init__.py new file mode 100644 index 00000000000..351e8fe48a0 --- /dev/null +++ b/debuginfo-tests/dexter/dex/tools/help/__init__.py @@ -0,0 +1,8 @@ +# 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.tools.help.Tool import Tool diff --git a/debuginfo-tests/dexter/dex/tools/list_debuggers/Tool.py b/debuginfo-tests/dexter/dex/tools/list_debuggers/Tool.py new file mode 100644 index 00000000000..5ef5d65464f --- /dev/null +++ b/debuginfo-tests/dexter/dex/tools/list_debuggers/Tool.py @@ -0,0 +1,40 @@ +# 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 +"""List debuggers tool.""" + +from dex.debugger.Debuggers import add_debugger_tool_base_arguments +from dex.debugger.Debuggers import handle_debugger_tool_base_options +from dex.debugger.Debuggers import Debuggers +from dex.tools import ToolBase +from dex.utils import Timer +from dex.utils.Exceptions import DebuggerException, Error +from dex.utils.ReturnCode import ReturnCode + + +class Tool(ToolBase): + """List all of the potential debuggers that DExTer knows about and whether + there is currently a valid interface available for them. + """ + + @property + def name(self): + return 'DExTer list debuggers' + + def add_tool_arguments(self, parser, defaults): + parser.description = Tool.__doc__ + add_debugger_tool_base_arguments(parser, defaults) + + def handle_options(self, defaults): + handle_debugger_tool_base_options(self.context, defaults) + + def go(self) -> ReturnCode: + with Timer('list debuggers'): + try: + Debuggers(self.context).list() + except DebuggerException as e: + raise Error(e) + return ReturnCode.OK diff --git a/debuginfo-tests/dexter/dex/tools/list_debuggers/__init__.py b/debuginfo-tests/dexter/dex/tools/list_debuggers/__init__.py new file mode 100644 index 00000000000..95741028be5 --- /dev/null +++ b/debuginfo-tests/dexter/dex/tools/list_debuggers/__init__.py @@ -0,0 +1,8 @@ +# 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.tools.list_debuggers.Tool import Tool diff --git a/debuginfo-tests/dexter/dex/tools/no_tool_/Tool.py b/debuginfo-tests/dexter/dex/tools/no_tool_/Tool.py new file mode 100644 index 00000000000..3d73189cd5b --- /dev/null +++ b/debuginfo-tests/dexter/dex/tools/no_tool_/Tool.py @@ -0,0 +1,49 @@ +# 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 +"""This is a special subtool that is run when no subtool is specified. +It just provides a welcome message and simple usage instructions. +""" + +from dex.tools import ToolBase, get_tool_names +from dex.utils.Exceptions import Error +from dex.utils.ReturnCode import ReturnCode + + +# This is a special "tool" that is run when no subtool has been specified on +# the command line. Its only job is to provide useful usage info. +class Tool(ToolBase): + """Welcome to DExTer (Debugging Experience Tester). + Please choose a subtool from the list below. Use 'dexter.py help' for more + information. + """ + + @property + def name(self): + return 'DExTer' + + def add_tool_arguments(self, parser, defaults): + parser.description = Tool.__doc__ + parser.add_argument( + 'subtool', + choices=[t for t in get_tool_names() if not t.endswith('-')], + nargs='?', + help='name of subtool') + parser.add_argument( + 'subtool_options', + metavar='subtool-options', + nargs='*', + help='subtool specific options') + + def handle_options(self, defaults): + if not self.context.options.subtool: + raise Error('<d>no subtool specified</>\n\n{}\n'.format( + self.parser.format_help())) + + def go(self) -> ReturnCode: + # This fn is never called because not specifying a subtool raises an + # exception. + return ReturnCode._ERROR diff --git a/debuginfo-tests/dexter/dex/tools/no_tool_/__init__.py b/debuginfo-tests/dexter/dex/tools/no_tool_/__init__.py new file mode 100644 index 00000000000..0e355f818aa --- /dev/null +++ b/debuginfo-tests/dexter/dex/tools/no_tool_/__init__.py @@ -0,0 +1,8 @@ +# 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.tools.no_tool_.Tool import Tool diff --git a/debuginfo-tests/dexter/dex/tools/run_debugger_internal_/Tool.py b/debuginfo-tests/dexter/dex/tools/run_debugger_internal_/Tool.py new file mode 100644 index 00000000000..494b4e1d0a9 --- /dev/null +++ b/debuginfo-tests/dexter/dex/tools/run_debugger_internal_/Tool.py @@ -0,0 +1,74 @@ +# 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 +"""This is an internal subtool used to sandbox the communication with a +debugger into a separate process so that any crashes inside the debugger will +not bring down the entire DExTer tool. +""" + +import pickle + +from dex.debugger import Debuggers +from dex.tools import ToolBase +from dex.utils import Timer +from dex.utils.Exceptions import DebuggerException, Error +from dex.utils.ReturnCode import ReturnCode + + +class Tool(ToolBase): + def __init__(self, *args, **kwargs): + super(Tool, self).__init__(*args, **kwargs) + self.dextIR = None + + @property + def name(self): + return 'DExTer run debugger internal' + + def add_tool_arguments(self, parser, defaults): + parser.add_argument('dextIR_path', type=str, help='dextIR file') + parser.add_argument( + 'pickled_options', type=str, help='pickled options file') + + def handle_options(self, defaults): + with open(self.context.options.dextIR_path, 'rb') as fp: + self.dextIR = pickle.load(fp) + + with open(self.context.options.pickled_options, 'rb') as fp: + poptions = pickle.load(fp) + poptions.working_directory = ( + self.context.options.working_directory[:]) + poptions.unittest = self.context.options.unittest + poptions.dextIR_path = self.context.options.dextIR_path + self.context.options = poptions + + Timer.display = self.context.options.time_report + + def go(self) -> ReturnCode: + options = self.context.options + + with Timer('loading debugger'): + debugger = Debuggers(self.context).load(options.debugger, + self.dextIR) + self.dextIR.debugger = debugger.debugger_info + + with Timer('running debugger'): + if not debugger.is_available: + msg = '<d>could not load {}</> ({})\n'.format( + debugger.name, debugger.loading_error) + if options.verbose: + msg = '{}\n {}'.format( + msg, ' '.join(debugger.loading_error_trace)) + raise Error(msg) + + with debugger: + try: + debugger.start() + except DebuggerException as e: + raise Error(e) + + with open(self.context.options.dextIR_path, 'wb') as fp: + pickle.dump(self.dextIR, fp) + return ReturnCode.OK diff --git a/debuginfo-tests/dexter/dex/tools/run_debugger_internal_/__init__.py b/debuginfo-tests/dexter/dex/tools/run_debugger_internal_/__init__.py new file mode 100644 index 00000000000..db3f98bd75b --- /dev/null +++ b/debuginfo-tests/dexter/dex/tools/run_debugger_internal_/__init__.py @@ -0,0 +1,8 @@ +# 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.tools.run_debugger_internal_.Tool import Tool diff --git a/debuginfo-tests/dexter/dex/tools/test/Tool.py b/debuginfo-tests/dexter/dex/tools/test/Tool.py new file mode 100644 index 00000000000..fcd009c5081 --- /dev/null +++ b/debuginfo-tests/dexter/dex/tools/test/Tool.py @@ -0,0 +1,244 @@ +# 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 +"""Test tool.""" + +import os +import csv +import pickle +import shutil + +from dex.builder import run_external_build_script +from dex.debugger.Debuggers import get_debugger_steps +from dex.heuristic import Heuristic +from dex.tools import TestToolBase +from dex.utils.Exceptions import DebuggerException +from dex.utils.Exceptions import BuildScriptException, HeuristicException +from dex.utils.PrettyOutputBase import Stream +from dex.utils.ReturnCode import ReturnCode +from dex.dextIR import BuilderIR + + +class TestCase(object): + def __init__(self, context, name, heuristic, error): + self.context = context + self.name = name + self.heuristic = heuristic + self.error = error + + @property + def penalty(self): + try: + return self.heuristic.penalty + except AttributeError: + return float('nan') + + @property + def max_penalty(self): + try: + return self.heuristic.max_penalty + except AttributeError: + return float('nan') + + @property + def score(self): + try: + return self.heuristic.score + except AttributeError: + return float('nan') + + def __str__(self): + if self.error and self.context.options.verbose: + verbose_error = str(self.error) + else: + verbose_error = '' + + if self.error: + script_error = (' : {}'.format( + self.error.script_error.splitlines()[0].decode()) if getattr( + self.error, 'script_error', None) else '') + + error = ' [{}{}]'.format( + str(self.error).splitlines()[0], script_error) + else: + error = '' + + try: + summary = self.heuristic.summary_string + except AttributeError: + summary = '<r>nan/nan (nan)</>' + return '{}: {}{}\n{}'.format(self.name, summary, error, verbose_error) + + +class Tool(TestToolBase): + """Run the specified DExTer test(s) with the specified compiler and linker + options and produce a dextIR file as well as printing out the debugging + experience score calculated by the DExTer heuristic. + """ + + def __init__(self, *args, **kwargs): + super(Tool, self).__init__(*args, **kwargs) + self._test_cases = [] + + @property + def name(self): + return 'DExTer test' + + def add_tool_arguments(self, parser, defaults): + parser.add_argument('--fail-lt', + type=float, + default=0.0, # By default TEST always succeeds. + help='exit with status FAIL(2) if the test result' + ' is less than this value.', + metavar='<float>') + super(Tool, self).add_tool_arguments(parser, defaults) + + def _build_test_case(self): + """Build an executable from the test source with the given --builder + script and flags (--cflags, --ldflags) in the working directory. + Or, if the --binary option has been given, copy the executable provided + into the working directory and rename it to match the --builder output. + """ + + options = self.context.options + if options.binary: + # Copy user's binary into the tmp working directory + shutil.copy(options.binary, options.executable) + builderIR = BuilderIR( + name='binary', + cflags=[options.binary], + ldflags='') + else: + options = self.context.options + compiler_options = [options.cflags for _ in options.source_files] + linker_options = options.ldflags + _, _, builderIR = run_external_build_script( + self.context, + script_path=self.build_script, + source_files=options.source_files, + compiler_options=compiler_options, + linker_options=linker_options, + executable_file=options.executable) + return builderIR + + def _get_steps(self, builderIR): + """Generate a list of debugger steps from a test case. + """ + steps = get_debugger_steps(self.context) + steps.builder = builderIR + return steps + + def _get_results_basename(self, test_name): + def splitall(x): + while len(x) > 0: + x, y = os.path.split(x) + yield y + all_components = reversed([x for x in splitall(test_name)]) + return '_'.join(all_components) + + def _get_results_path(self, test_name): + """Returns the path to the test results directory for the test denoted + by test_name. + """ + return os.path.join(self.context.options.results_directory, + self._get_results_basename(test_name)) + + def _get_results_text_path(self, test_name): + """Returns path results .txt file for test denoted by test_name. + """ + test_results_path = self._get_results_path(test_name) + return '{}.txt'.format(test_results_path) + + def _get_results_pickle_path(self, test_name): + """Returns path results .dextIR file for test denoted by test_name. + """ + test_results_path = self._get_results_path(test_name) + return '{}.dextIR'.format(test_results_path) + + def _record_steps(self, test_name, steps): + """Write out the set of steps out to the test's .txt and .json + results file. + """ + output_text_path = self._get_results_text_path(test_name) + with open(output_text_path, 'w') as fp: + self.context.o.auto(str(steps), stream=Stream(fp)) + + output_dextIR_path = self._get_results_pickle_path(test_name) + with open(output_dextIR_path, 'wb') as fp: + pickle.dump(steps, fp, protocol=pickle.HIGHEST_PROTOCOL) + + def _record_score(self, test_name, heuristic): + """Write out the test's heuristic score to the results .txt file. + """ + output_text_path = self._get_results_text_path(test_name) + with open(output_text_path, 'a') as fp: + self.context.o.auto(heuristic.verbose_output, stream=Stream(fp)) + + def _record_test_and_display(self, test_case): + """Output test case to o stream and record test case internally for + handling later. + """ + self.context.o.auto(test_case) + self._test_cases.append(test_case) + + def _record_failed_test(self, test_name, exception): + """Instantiate a failed test case with failure exception and + store internally. + """ + test_case = TestCase(self.context, test_name, None, exception) + self._record_test_and_display(test_case) + + def _record_successful_test(self, test_name, steps, heuristic): + """Instantiate a successful test run, store test for handling later. + Display verbose output for test case if required. + """ + test_case = TestCase(self.context, test_name, heuristic, None) + self._record_test_and_display(test_case) + if self.context.options.verbose: + self.context.o.auto('\n{}\n'.format(steps)) + self.context.o.auto(heuristic.verbose_output) + + def _run_test(self, test_name): + """Attempt to run test files specified in options.source_files. Store + result internally in self._test_cases. + """ + try: + builderIR = self._build_test_case() + steps = self._get_steps(builderIR) + self._record_steps(test_name, steps) + heuristic_score = Heuristic(self.context, steps) + self._record_score(test_name, heuristic_score) + except (BuildScriptException, DebuggerException, + HeuristicException) as e: + self._record_failed_test(test_name, e) + return + + self._record_successful_test(test_name, steps, heuristic_score) + return + + def _handle_results(self) -> ReturnCode: + return_code = ReturnCode.OK + options = self.context.options + + if not options.verbose: + self.context.o.auto('\n') + + summary_path = os.path.join(options.results_directory, 'summary.csv') + with open(summary_path, mode='w', newline='') as fp: + writer = csv.writer(fp, delimiter=',') + writer.writerow(['Test Case', 'Score', 'Error']) + + for test_case in self._test_cases: + if (test_case.score < options.fail_lt or + test_case.error is not None): + return_code = ReturnCode.FAIL + + writer.writerow([ + test_case.name, '{:.4f}'.format(test_case.score), + test_case.error + ]) + + return return_code diff --git a/debuginfo-tests/dexter/dex/tools/test/__init__.py b/debuginfo-tests/dexter/dex/tools/test/__init__.py new file mode 100644 index 00000000000..01ead3affe1 --- /dev/null +++ b/debuginfo-tests/dexter/dex/tools/test/__init__.py @@ -0,0 +1,8 @@ +# 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.tools.test.Tool import Tool diff --git a/debuginfo-tests/dexter/dex/tools/view/Tool.py b/debuginfo-tests/dexter/dex/tools/view/Tool.py new file mode 100644 index 00000000000..ad7d5300035 --- /dev/null +++ b/debuginfo-tests/dexter/dex/tools/view/Tool.py @@ -0,0 +1,59 @@ +# 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 +"""View tool.""" + +import os + +import pickle +from dex.heuristic import Heuristic +from dex.heuristic.Heuristic import add_heuristic_tool_arguments +from dex.tools import ToolBase +from dex.utils.Exceptions import Error, HeuristicException +from dex.utils.ReturnCode import ReturnCode + + +class Tool(ToolBase): + """Given a dextIR file, display the information in a human-readable form. + """ + + @property + def name(self): + return 'DExTer view' + + def add_tool_arguments(self, parser, defaults): + add_heuristic_tool_arguments(parser) + parser.add_argument( + 'input_path', + metavar='dextIR-file', + type=str, + default=None, + help='dexter dextIR file to view') + parser.description = Tool.__doc__ + + def handle_options(self, defaults): + options = self.context.options + + options.input_path = os.path.abspath(options.input_path) + if not os.path.isfile(options.input_path): + raise Error('<d>could not find dextIR file</> <r>"{}"</>'.format( + options.input_path)) + + def go(self) -> ReturnCode: + options = self.context.options + + with open(options.input_path, 'rb') as fp: + steps = pickle.load(fp) + + try: + heuristic = Heuristic(self.context, steps) + except HeuristicException as e: + raise Error('could not apply heuristic: {}'.format(e)) + + self.context.o.auto('{}\n\n{}\n\n{}\n\n'.format( + heuristic.summary_string, steps, heuristic.verbose_output)) + + return ReturnCode.OK diff --git a/debuginfo-tests/dexter/dex/tools/view/__init__.py b/debuginfo-tests/dexter/dex/tools/view/__init__.py new file mode 100644 index 00000000000..1868fca28c2 --- /dev/null +++ b/debuginfo-tests/dexter/dex/tools/view/__init__.py @@ -0,0 +1,8 @@ +# 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.tools.view.Tool import Tool diff --git a/debuginfo-tests/dexter/dex/utils/Environment.py b/debuginfo-tests/dexter/dex/utils/Environment.py new file mode 100644 index 00000000000..d2df2522440 --- /dev/null +++ b/debuginfo-tests/dexter/dex/utils/Environment.py @@ -0,0 +1,22 @@ +# 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 +"""Utility functions for querying the current environment.""" + +import os + + +def is_native_windows(): + return os.name == 'nt' + + +def has_pywin32(): + try: + import win32com.client # pylint:disable=unused-variable + import win32api # pylint:disable=unused-variable + return True + except ImportError: + return False diff --git a/debuginfo-tests/dexter/dex/utils/Exceptions.py b/debuginfo-tests/dexter/dex/utils/Exceptions.py new file mode 100644 index 00000000000..39c0c2f1695 --- /dev/null +++ b/debuginfo-tests/dexter/dex/utils/Exceptions.py @@ -0,0 +1,72 @@ +# 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 +"""Provides Dexter-specific exception types.""" + + +class Dexception(Exception): + """All dexter-specific exceptions derive from this.""" + pass + + +class Error(Dexception): + """Error. Prints 'error: <message>' without a traceback.""" + pass + + +class DebuggerException(Dexception): + """Any error from using the debugger.""" + + def __init__(self, msg, orig_exception=None): + super(DebuggerException, self).__init__(msg) + self.msg = msg + self.orig_exception = orig_exception + + def __str__(self): + return str(self.msg) + + +class LoadDebuggerException(DebuggerException): + """If specified debugger cannot be loaded.""" + pass + + +class NotYetLoadedDebuggerException(LoadDebuggerException): + """If specified debugger has not yet been attempted to load.""" + + def __init__(self): + super(NotYetLoadedDebuggerException, + self).__init__('not loaded', orig_exception=None) + + +class CommandParseError(Dexception): + """If a command instruction cannot be successfully parsed.""" + + def __init__(self, *args, **kwargs): + super(CommandParseError, self).__init__(*args, **kwargs) + self.filename = None + self.lineno = None + self.info = None + self.src = None + self.caret = None + + +class ToolArgumentError(Dexception): + """If a tool argument is invalid.""" + pass + + +class BuildScriptException(Dexception): + """If there is an error in a build script file.""" + + def __init__(self, *args, **kwargs): + self.script_error = kwargs.pop('script_error', None) + super(BuildScriptException, self).__init__(*args, **kwargs) + + +class HeuristicException(Dexception): + """If there was a problem with the heuristic.""" + pass diff --git a/debuginfo-tests/dexter/dex/utils/ExtArgParse.py b/debuginfo-tests/dexter/dex/utils/ExtArgParse.py new file mode 100644 index 00000000000..9fa08fb066e --- /dev/null +++ b/debuginfo-tests/dexter/dex/utils/ExtArgParse.py @@ -0,0 +1,148 @@ +# 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 +"""Extended Argument Parser. Extends the argparse module with some extra +functionality, to hopefully aid user-friendliness. +""" + +import argparse +import difflib +import unittest + +from dex.utils import PrettyOutput +from dex.utils.Exceptions import Error + +# re-export all of argparse +for argitem in argparse.__all__: + vars()[argitem] = getattr(argparse, argitem) + + +def _did_you_mean(val, possibles): + close_matches = difflib.get_close_matches(val, possibles) + did_you_mean = '' + if close_matches: + did_you_mean = 'did you mean {}?'.format(' or '.join( + "<y>'{}'</>".format(c) for c in close_matches[:2])) + return did_you_mean + + +def _colorize(message): + lines = message.splitlines() + for i, line in enumerate(lines): + lines[i] = lines[i].replace('usage:', '<g>usage:</>') + if line.endswith(':'): + lines[i] = '<g>{}</>'.format(line) + return '\n'.join(lines) + + +class ExtArgumentParser(argparse.ArgumentParser): + def error(self, message): + """Use the Dexception Error mechanism (including auto-colored output). + """ + raise Error('{}\n\n{}'.format(message, self.format_usage())) + + # pylint: disable=redefined-builtin + def _print_message(self, message, file=None): + if message: + if file and file.name == '<stdout>': + file = PrettyOutput.stdout + else: + file = PrettyOutput.stderr + + self.context.o.auto(message, file) + + # pylint: enable=redefined-builtin + + def format_usage(self): + return _colorize(super(ExtArgumentParser, self).format_usage()) + + def format_help(self): + return _colorize(super(ExtArgumentParser, self).format_help() + '\n\n') + + @property + def _valid_visible_options(self): + """A list of all non-suppressed command line flags.""" + return [ + item for sublist in vars(self)['_actions'] + for item in sublist.option_strings + if sublist.help != argparse.SUPPRESS + ] + + def parse_args(self, args=None, namespace=None): + """Add 'did you mean' output to errors.""" + args, argv = self.parse_known_args(args, namespace) + if argv: + errors = [] + for arg in argv: + if arg in self._valid_visible_options: + error = "unexpected argument: <y>'{}'</>".format(arg) + else: + error = "unrecognized argument: <y>'{}'</>".format(arg) + dym = _did_you_mean(arg, self._valid_visible_options) + if dym: + error += ' ({})'.format(dym) + errors.append(error) + self.error('\n '.join(errors)) + + return args + + def add_argument(self, *args, **kwargs): + """Automatically add the default value to help text.""" + if 'default' in kwargs: + default = kwargs['default'] + if default is None: + default = kwargs.pop('display_default', None) + + if (default and isinstance(default, (str, int, float)) + and default != argparse.SUPPRESS): + assert ( + 'choices' not in kwargs or default in kwargs['choices']), ( + "default value '{}' is not one of allowed choices: {}". + format(default, kwargs['choices'])) + if 'help' in kwargs and kwargs['help'] != argparse.SUPPRESS: + assert isinstance(kwargs['help'], str), type(kwargs['help']) + kwargs['help'] = ('{} (default:{})'.format( + kwargs['help'], default)) + + super(ExtArgumentParser, self).add_argument(*args, **kwargs) + + def __init__(self, context, *args, **kwargs): + self.context = context + super(ExtArgumentParser, self).__init__(*args, **kwargs) + + +class TestExtArgumentParser(unittest.TestCase): + def test_did_you_mean(self): + parser = ExtArgumentParser(None) + parser.add_argument('--foo') + parser.add_argument('--qoo', help=argparse.SUPPRESS) + parser.add_argument('jam', nargs='?') + + parser.parse_args(['--foo', '0']) + + expected = (r"^unrecognized argument\: <y>'\-\-doo'</>\s+" + r"\(did you mean <y>'\-\-foo'</>\?\)\n" + r"\s*<g>usage:</>") + with self.assertRaisesRegex(Error, expected): + parser.parse_args(['--doo']) + + parser.add_argument('--noo') + + expected = (r"^unrecognized argument\: <y>'\-\-doo'</>\s+" + r"\(did you mean <y>'\-\-noo'</> or <y>'\-\-foo'</>\?\)\n" + r"\s*<g>usage:</>") + with self.assertRaisesRegex(Error, expected): + parser.parse_args(['--doo']) + + expected = (r"^unrecognized argument\: <y>'\-\-bar'</>\n" + r"\s*<g>usage:</>") + with self.assertRaisesRegex(Error, expected): + parser.parse_args(['--bar']) + + expected = (r"^unexpected argument\: <y>'\-\-foo'</>\n" + r"\s*<g>usage:</>") + with self.assertRaisesRegex(Error, expected): + parser.parse_args(['--', 'x', '--foo']) diff --git a/debuginfo-tests/dexter/dex/utils/PrettyOutputBase.py b/debuginfo-tests/dexter/dex/utils/PrettyOutputBase.py new file mode 100644 index 00000000000..d21db89a6ae --- /dev/null +++ b/debuginfo-tests/dexter/dex/utils/PrettyOutputBase.py @@ -0,0 +1,392 @@ +# 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 +"""Provides formatted/colored console output on both Windows and Linux. + +Do not use this module directly, but instead use via the appropriate platform- +specific module. +""" + +import abc +import re +import sys +import threading +import unittest + +from io import StringIO + +from dex.utils.Exceptions import Error + + +class _NullLock(object): + def __enter__(self): + return None + + def __exit__(self, *params): + pass + + +_lock = threading.Lock() +_null_lock = _NullLock() + + +class PreserveAutoColors(object): + def __init__(self, pretty_output): + self.pretty_output = pretty_output + self.orig_values = {} + self.properties = [ + 'auto_reds', 'auto_yellows', 'auto_greens', 'auto_blues' + ] + + def __enter__(self): + for p in self.properties: + self.orig_values[p] = getattr(self.pretty_output, p)[:] + return self + + def __exit__(self, *args): + for p in self.properties: + setattr(self.pretty_output, p, self.orig_values[p]) + + +class Stream(object): + def __init__(self, py_, os_=None): + self.py = py_ + self.os = os_ + self.orig_color = None + self.color_enabled = self.py.isatty() + + +class PrettyOutputBase(object, metaclass=abc.ABCMeta): + stdout = Stream(sys.stdout) + stderr = Stream(sys.stderr) + + def __init__(self): + self.auto_reds = [] + self.auto_yellows = [] + self.auto_greens = [] + self.auto_blues = [] + self._stack = [] + + def __enter__(self): + return self + + def __exit__(self, *args): + pass + + def _set_valid_stream(self, stream): + if stream is None: + return self.__class__.stdout + return stream + + def _write(self, text, stream): + text = str(text) + + # Users can embed color control tags in their output + # (e.g. <r>hello</> <y>world</> would write the word 'hello' in red and + # 'world' in yellow). + # This function parses these tags using a very simple recursive + # descent. + colors = { + 'r': self.red, + 'y': self.yellow, + 'g': self.green, + 'b': self.blue, + 'd': self.default, + 'a': self.auto, + } + + # Find all tags (whether open or close) + tags = [ + t for t in re.finditer('<([{}/])>'.format(''.join(colors)), text) + ] + + if not tags: + # No tags. Just write the text to the current stream and return. + # 'unmangling' any tags that have been mangled so that they won't + # render as colors (for example in error output from this + # function). + stream = self._set_valid_stream(stream) + stream.py.write(text.replace(r'\>', '>')) + return + + open_tags = [i for i in tags if i.group(1) != '/'] + close_tags = [i for i in tags if i.group(1) == '/'] + + if (len(open_tags) != len(close_tags) + or any(o.start() >= c.start() + for (o, c) in zip(open_tags, close_tags))): + raise Error('open/close tag mismatch in "{}"'.format( + text.rstrip()).replace('>', r'\>')) + + open_tag = open_tags.pop(0) + + # We know that the tags balance correctly, so figure out where the + # corresponding close tag is to the current open tag. + tag_nesting = 1 + close_tag = None + for tag in tags[1:]: + if tag.group(1) == '/': + tag_nesting -= 1 + else: + tag_nesting += 1 + if tag_nesting == 0: + close_tag = tag + break + else: + assert False, text + + # Use the method on the top of the stack for text prior to the open + # tag. + before = text[:open_tag.start()] + if before: + self._stack[-1](before, lock=_null_lock, stream=stream) + + # Use the specified color for the tag itself. + color = open_tag.group(1) + within = text[open_tag.end():close_tag.start()] + if within: + colors[color](within, lock=_null_lock, stream=stream) + + # Use the method on the top of the stack for text after the close tag. + after = text[close_tag.end():] + if after: + self._stack[-1](after, lock=_null_lock, stream=stream) + + def flush(self, stream): + stream = self._set_valid_stream(stream) + stream.py.flush() + + def auto(self, text, stream=None, lock=_lock): + text = str(text) + stream = self._set_valid_stream(stream) + lines = text.splitlines(True) + + with lock: + for line in lines: + # This is just being cute for the sake of cuteness, but why + # not? + line = line.replace('DExTer', '<r>D<y>E<g>x<b>T</></>e</>r</>') + + # Apply the appropriate color method if the expression matches + # any of + # the patterns we have set up. + for fn, regexs in ((self.red, self.auto_reds), + (self.yellow, self.auto_yellows), + (self.green, + self.auto_greens), (self.blue, + self.auto_blues)): + if any(re.search(regex, line) for regex in regexs): + fn(line, stream=stream, lock=_null_lock) + break + else: + self.default(line, stream=stream, lock=_null_lock) + + def _call_color_impl(self, fn, impl, text, *args, **kwargs): + try: + self._stack.append(fn) + return impl(text, *args, **kwargs) + finally: + fn = self._stack.pop() + + @abc.abstractmethod + def red_impl(self, text, stream=None, **kwargs): + pass + + def red(self, *args, **kwargs): + return self._call_color_impl(self.red, self.red_impl, *args, **kwargs) + + @abc.abstractmethod + def yellow_impl(self, text, stream=None, **kwargs): + pass + + def yellow(self, *args, **kwargs): + return self._call_color_impl(self.yellow, self.yellow_impl, *args, + **kwargs) + + @abc.abstractmethod + def green_impl(self, text, stream=None, **kwargs): + pass + + def green(self, *args, **kwargs): + return self._call_color_impl(self.green, self.green_impl, *args, + **kwargs) + + @abc.abstractmethod + def blue_impl(self, text, stream=None, **kwargs): + pass + + def blue(self, *args, **kwargs): + return self._call_color_impl(self.blue, self.blue_impl, *args, + **kwargs) + + @abc.abstractmethod + def default_impl(self, text, stream=None, **kwargs): + pass + + def default(self, *args, **kwargs): + return self._call_color_impl(self.default, self.default_impl, *args, + **kwargs) + + def colortest(self): + from itertools import combinations, permutations + + fns = ((self.red, 'rrr'), (self.yellow, 'yyy'), (self.green, 'ggg'), + (self.blue, 'bbb'), (self.default, 'ddd')) + + for l in range(1, len(fns) + 1): + for comb in combinations(fns, l): + for perm in permutations(comb): + for stream in (None, self.__class__.stderr): + perm[0][0]('stdout ' + if stream is None else 'stderr ', stream) + for fn, string in perm: + fn(string, stream) + self.default('\n', stream) + + tests = [ + (self.auto, 'default1<r>red2</>default3'), + (self.red, 'red1<r>red2</>red3'), + (self.blue, 'blue1<r>red2</>blue3'), + (self.red, 'red1<y>yellow2</>red3'), + (self.auto, 'default1<y>yellow2<r>red3</></>'), + (self.auto, 'default1<g>green2<r>red3</></>'), + (self.auto, 'default1<g>green2<r>red3</>green4</>default5'), + (self.auto, 'default1<g>green2</>default3<g>green4</>default5'), + (self.auto, '<r>red1<g>green2</>red3<g>green4</>red5</>'), + (self.auto, '<r>red1<y><g>green2</>yellow3</>green4</>default5'), + (self.auto, '<r><y><g><b><d>default1</></><r></></></>red2</>'), + (self.auto, '<r>red1</>default2<r>red3</><g>green4</>default5'), + (self.blue, '<r>red1</>blue2<r><r>red3</><g><g>green</></></>'), + (self.blue, '<r>r<r>r<y>y<r><r><r><r>r</></></></></></></>b'), + ] + + for fn, text in tests: + for stream in (None, self.__class__.stderr): + stream_name = 'stdout' if stream is None else 'stderr' + fn('{} {}\n'.format(stream_name, text), stream) + + +class TestPrettyOutput(unittest.TestCase): + class MockPrettyOutput(PrettyOutputBase): + def red_impl(self, text, stream=None, **kwargs): + self._write('[R]{}[/R]'.format(text), stream) + + def yellow_impl(self, text, stream=None, **kwargs): + self._write('[Y]{}[/Y]'.format(text), stream) + + def green_impl(self, text, stream=None, **kwargs): + self._write('[G]{}[/G]'.format(text), stream) + + def blue_impl(self, text, stream=None, **kwargs): + self._write('[B]{}[/B]'.format(text), stream) + + def default_impl(self, text, stream=None, **kwargs): + self._write('[D]{}[/D]'.format(text), stream) + + def test_red(self): + with TestPrettyOutput.MockPrettyOutput() as o: + stream = Stream(StringIO()) + o.red('hello', stream) + self.assertEqual(stream.py.getvalue(), '[R]hello[/R]') + + def test_yellow(self): + with TestPrettyOutput.MockPrettyOutput() as o: + stream = Stream(StringIO()) + o.yellow('hello', stream) + self.assertEqual(stream.py.getvalue(), '[Y]hello[/Y]') + + def test_green(self): + with TestPrettyOutput.MockPrettyOutput() as o: + stream = Stream(StringIO()) + o.green('hello', stream) + self.assertEqual(stream.py.getvalue(), '[G]hello[/G]') + + def test_blue(self): + with TestPrettyOutput.MockPrettyOutput() as o: + stream = Stream(StringIO()) + o.blue('hello', stream) + self.assertEqual(stream.py.getvalue(), '[B]hello[/B]') + + def test_default(self): + with TestPrettyOutput.MockPrettyOutput() as o: + stream = Stream(StringIO()) + o.default('hello', stream) + self.assertEqual(stream.py.getvalue(), '[D]hello[/D]') + + def test_auto(self): + with TestPrettyOutput.MockPrettyOutput() as o: + stream = Stream(StringIO()) + o.auto_reds.append('foo') + o.auto('bar\n', stream) + o.auto('foo\n', stream) + o.auto('baz\n', stream) + self.assertEqual(stream.py.getvalue(), + '[D]bar\n[/D][R]foo\n[/R][D]baz\n[/D]') + + stream = Stream(StringIO()) + o.auto('bar\nfoo\nbaz\n', stream) + self.assertEqual(stream.py.getvalue(), + '[D]bar\n[/D][R]foo\n[/R][D]baz\n[/D]') + + stream = Stream(StringIO()) + o.auto('barfoobaz\nbardoobaz\n', stream) + self.assertEqual(stream.py.getvalue(), + '[R]barfoobaz\n[/R][D]bardoobaz\n[/D]') + + o.auto_greens.append('doo') + stream = Stream(StringIO()) + o.auto('barfoobaz\nbardoobaz\n', stream) + self.assertEqual(stream.py.getvalue(), + '[R]barfoobaz\n[/R][G]bardoobaz\n[/G]') + + def test_PreserveAutoColors(self): + with TestPrettyOutput.MockPrettyOutput() as o: + o.auto_reds.append('foo') + with PreserveAutoColors(o): + o.auto_greens.append('bar') + stream = Stream(StringIO()) + o.auto('foo\nbar\nbaz\n', stream) + self.assertEqual(stream.py.getvalue(), + '[R]foo\n[/R][G]bar\n[/G][D]baz\n[/D]') + + stream = Stream(StringIO()) + o.auto('foo\nbar\nbaz\n', stream) + self.assertEqual(stream.py.getvalue(), + '[R]foo\n[/R][D]bar\n[/D][D]baz\n[/D]') + + stream = Stream(StringIO()) + o.yellow('<a>foo</>bar<a>baz</>', stream) + self.assertEqual( + stream.py.getvalue(), + '[Y][Y][/Y][R]foo[/R][Y][Y]bar[/Y][D]baz[/D][Y][/Y][/Y][/Y]') + + def test_tags(self): + with TestPrettyOutput.MockPrettyOutput() as o: + stream = Stream(StringIO()) + o.auto('<r>hi</>', stream) + self.assertEqual(stream.py.getvalue(), + '[D][D][/D][R]hi[/R][D][/D][/D]') + + stream = Stream(StringIO()) + o.auto('<r><y>a</>b</>c', stream) + self.assertEqual( + stream.py.getvalue(), + '[D][D][/D][R][R][/R][Y]a[/Y][R]b[/R][/R][D]c[/D][/D]') + + with self.assertRaisesRegex(Error, 'tag mismatch'): + o.auto('<r>hi', stream) + + with self.assertRaisesRegex(Error, 'tag mismatch'): + o.auto('hi</>', stream) + + with self.assertRaisesRegex(Error, 'tag mismatch'): + o.auto('<r><y>hi</>', stream) + + with self.assertRaisesRegex(Error, 'tag mismatch'): + o.auto('<r><y>hi</><r></>', stream) + + with self.assertRaisesRegex(Error, 'tag mismatch'): + o.auto('</>hi<r>', stream) diff --git a/debuginfo-tests/dexter/dex/utils/ReturnCode.py b/debuginfo-tests/dexter/dex/utils/ReturnCode.py new file mode 100644 index 00000000000..487d225d1b6 --- /dev/null +++ b/debuginfo-tests/dexter/dex/utils/ReturnCode.py @@ -0,0 +1,20 @@ +# 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 enum import Enum + + +class ReturnCode(Enum): + """Used to indicate whole program success status.""" + + OK = 0 + _ERROR = 1 # Unhandled exceptions result in exit(1) by default. + # Usage of _ERROR is discouraged: + # If the program cannot run, raise an exception. + # If the program runs successfully but the result is + # "failure" based on the inputs, return FAIL + FAIL = 2 diff --git a/debuginfo-tests/dexter/dex/utils/RootDirectory.py b/debuginfo-tests/dexter/dex/utils/RootDirectory.py new file mode 100644 index 00000000000..57f204c79ac --- /dev/null +++ b/debuginfo-tests/dexter/dex/utils/RootDirectory.py @@ -0,0 +1,15 @@ +# 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 +"""Utility functions related to DExTer's directory layout.""" + +import os + + +def get_root_directory(): + root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) + assert os.path.basename(root) == 'dex', root + return root diff --git a/debuginfo-tests/dexter/dex/utils/Timer.py b/debuginfo-tests/dexter/dex/utils/Timer.py new file mode 100644 index 00000000000..63726f1a757 --- /dev/null +++ b/debuginfo-tests/dexter/dex/utils/Timer.py @@ -0,0 +1,50 @@ +# 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 +"""RAII-style timer class to be used with a 'with' statement to get wall clock +time for the contained code. +""" + +import sys +import time + + +def _indent(indent): + return '| ' * indent + + +class Timer(object): + fn = sys.stdout.write + display = False + indent = 0 + + def __init__(self, name=None): + self.name = name + self.start = self.now + + def __enter__(self): + Timer.indent += 1 + if Timer.display and self.name: + indent = _indent(Timer.indent - 1) + ' _' + Timer.fn('{}\n'.format(_indent(Timer.indent - 1))) + Timer.fn('{} start {}\n'.format(indent, self.name)) + return self + + def __exit__(self, *args): + if Timer.display and self.name: + indent = _indent(Timer.indent - 1) + '|_' + Timer.fn('{} {} time taken: {:0.1f}s\n'.format( + indent, self.name, self.elapsed)) + Timer.fn('{}\n'.format(_indent(Timer.indent - 1))) + Timer.indent -= 1 + + @property + def elapsed(self): + return self.now - self.start + + @property + def now(self): + return time.time() diff --git a/debuginfo-tests/dexter/dex/utils/UnitTests.py b/debuginfo-tests/dexter/dex/utils/UnitTests.py new file mode 100644 index 00000000000..5a8a0a6aeb9 --- /dev/null +++ b/debuginfo-tests/dexter/dex/utils/UnitTests.py @@ -0,0 +1,62 @@ +# 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 +"""Unit test harness.""" + +from fnmatch import fnmatch +import os +import unittest + +from io import StringIO + +from dex.utils import is_native_windows, has_pywin32 +from dex.utils import PreserveAutoColors, PrettyOutput +from dex.utils import Timer + + +class DexTestLoader(unittest.TestLoader): + def _match_path(self, path, full_path, pattern): + """Don't try to import platform-specific modules for the wrong platform + during test discovery. + """ + d = os.path.basename(os.path.dirname(full_path)) + if is_native_windows(): + if d == 'posix': + return False + if d == 'windows': + return has_pywin32() + else: + if d == 'windows': + return False + return fnmatch(path, pattern) + + +def unit_tests_ok(context): + unittest.TestCase.maxDiff = None # remove size limit from diff output. + + with Timer('unit tests'): + suite = DexTestLoader().discover( + context.root_directory, pattern='*.py') + stream = StringIO() + result = unittest.TextTestRunner(verbosity=2, stream=stream).run(suite) + + ok = result.wasSuccessful() + if not ok or context.options.unittest == 'show-all': + with PreserveAutoColors(context.o): + context.o.auto_reds.extend( + [r'FAIL(ED|\:)', r'\.\.\.\s(FAIL|ERROR)$']) + context.o.auto_greens.extend([r'^OK$', r'\.\.\.\sok$']) + context.o.auto_blues.extend([r'^Ran \d+ test']) + context.o.default('\n') + for line in stream.getvalue().splitlines(True): + context.o.auto(line, stream=PrettyOutput.stderr) + + return ok + + +class TestUnitTests(unittest.TestCase): + def test_sanity(self): + self.assertEqual(1, 1) diff --git a/debuginfo-tests/dexter/dex/utils/Version.py b/debuginfo-tests/dexter/dex/utils/Version.py new file mode 100644 index 00000000000..1a257fa7107 --- /dev/null +++ b/debuginfo-tests/dexter/dex/utils/Version.py @@ -0,0 +1,40 @@ +# 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 +"""DExTer version output.""" + +import os +from subprocess import CalledProcessError, check_output, STDOUT +import sys + +from dex import __version__ + + +def _git_version(): + dir_ = os.path.dirname(__file__) + try: + branch = (check_output( + ['git', 'rev-parse', '--abbrev-ref', 'HEAD'], + stderr=STDOUT, + cwd=dir_).rstrip().decode('utf-8')) + hash_ = check_output( + ['git', 'rev-parse', 'HEAD'], stderr=STDOUT, + cwd=dir_).rstrip().decode('utf-8') + repo = check_output( + ['git', 'remote', 'get-url', 'origin'], stderr=STDOUT, + cwd=dir_).rstrip().decode('utf-8') + return '[{} {}] ({})'.format(branch, hash_, repo) + except (OSError, CalledProcessError): + pass + return None + + +def version(name): + lines = [] + lines.append(' '.join( + [s for s in [name, __version__, _git_version()] if s])) + lines.append(' using Python {}'.format(sys.version)) + return '\n'.join(lines) diff --git a/debuginfo-tests/dexter/dex/utils/Warning.py b/debuginfo-tests/dexter/dex/utils/Warning.py new file mode 100644 index 00000000000..402861aaed5 --- /dev/null +++ b/debuginfo-tests/dexter/dex/utils/Warning.py @@ -0,0 +1,18 @@ +# 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 +"""Utility functions for producing command line warnings.""" + + +def warn(context, msg, flag=None): + if context.options.no_warnings: + return + + msg = msg.rstrip() + if flag: + msg = '{} <y>[{}]</>'.format(msg, flag) + + context.o.auto('warning: <d>{}</>\n'.format(msg)) diff --git a/debuginfo-tests/dexter/dex/utils/WorkingDirectory.py b/debuginfo-tests/dexter/dex/utils/WorkingDirectory.py new file mode 100644 index 00000000000..e1862f2db72 --- /dev/null +++ b/debuginfo-tests/dexter/dex/utils/WorkingDirectory.py @@ -0,0 +1,46 @@ +# 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 +"""Create/set a temporary working directory for some operations.""" + +import os +import shutil +import tempfile +import time + +from dex.utils.Exceptions import Error + + +class WorkingDirectory(object): + def __init__(self, context, *args, **kwargs): + self.context = context + self.orig_cwd = os.getcwd() + + dir_ = kwargs.get('dir', None) + if dir_ and not os.path.isdir(dir_): + os.makedirs(dir_, exist_ok=True) + self.path = tempfile.mkdtemp(*args, **kwargs) + + def __enter__(self): + os.chdir(self.path) + return self + + def __exit__(self, *args): + os.chdir(self.orig_cwd) + if self.context.options.save_temps: + self.context.o.blue('"{}" left in place [--save-temps]\n'.format( + self.path)) + return + + exception = AssertionError('should never be raised') + for _ in range(100): + try: + shutil.rmtree(self.path) + return + except OSError as e: + exception = e + time.sleep(0.1) + raise Error(exception) diff --git a/debuginfo-tests/dexter/dex/utils/__init__.py b/debuginfo-tests/dexter/dex/utils/__init__.py new file mode 100644 index 00000000000..ac08139d568 --- /dev/null +++ b/debuginfo-tests/dexter/dex/utils/__init__.py @@ -0,0 +1,21 @@ +# 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 +"""Generic non-dexter-specific utility classes and functions.""" + +import os + +from dex.utils.Environment import is_native_windows, has_pywin32 +from dex.utils.PrettyOutputBase import PreserveAutoColors +from dex.utils.RootDirectory import get_root_directory +from dex.utils.Timer import Timer +from dex.utils.Warning import warn +from dex.utils.WorkingDirectory import WorkingDirectory + +if is_native_windows(): + from dex.utils.windows.PrettyOutput import PrettyOutput +else: + from dex.utils.posix.PrettyOutput import PrettyOutput diff --git a/debuginfo-tests/dexter/dex/utils/posix/PrettyOutput.py b/debuginfo-tests/dexter/dex/utils/posix/PrettyOutput.py new file mode 100644 index 00000000000..82cfed5dfd6 --- /dev/null +++ b/debuginfo-tests/dexter/dex/utils/posix/PrettyOutput.py @@ -0,0 +1,34 @@ +# 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 +"""Provides POSIX implementation of formatted/colored console output.""" + +from ..PrettyOutputBase import PrettyOutputBase, _lock + + +class PrettyOutput(PrettyOutputBase): + def _color(self, text, color, stream, lock=_lock): + """Use ANSI escape codes to provide color on Linux.""" + stream = self._set_valid_stream(stream) + with lock: + if stream.color_enabled: + text = '\033[{}m{}\033[0m'.format(color, text) + self._write(text, stream) + + def red_impl(self, text, stream=None, **kwargs): + self._color(text, 91, stream, **kwargs) + + def yellow_impl(self, text, stream=None, **kwargs): + self._color(text, 93, stream, **kwargs) + + def green_impl(self, text, stream=None, **kwargs): + self._color(text, 92, stream, **kwargs) + + def blue_impl(self, text, stream=None, **kwargs): + self._color(text, 96, stream, **kwargs) + + def default_impl(self, text, stream=None, **kwargs): + self._color(text, 0, stream, **kwargs) diff --git a/debuginfo-tests/dexter/dex/utils/posix/__init__.py b/debuginfo-tests/dexter/dex/utils/posix/__init__.py new file mode 100644 index 00000000000..1194affd891 --- /dev/null +++ b/debuginfo-tests/dexter/dex/utils/posix/__init__.py @@ -0,0 +1,6 @@ +# 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 diff --git a/debuginfo-tests/dexter/dex/utils/windows/PrettyOutput.py b/debuginfo-tests/dexter/dex/utils/windows/PrettyOutput.py new file mode 100644 index 00000000000..657406a59ac --- /dev/null +++ b/debuginfo-tests/dexter/dex/utils/windows/PrettyOutput.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 +"""Provides Windows implementation of formatted/colored console output.""" + +import sys + +import ctypes +import ctypes.wintypes + +from ..PrettyOutputBase import PrettyOutputBase, Stream, _lock, _null_lock + + +class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure): + # pylint: disable=protected-access + _fields_ = [('dwSize', ctypes.wintypes._COORD), ('dwCursorPosition', + ctypes.wintypes._COORD), + ('wAttributes', + ctypes.c_ushort), ('srWindow', ctypes.wintypes._SMALL_RECT), + ('dwMaximumWindowSize', ctypes.wintypes._COORD)] + # pylint: enable=protected-access + + +class PrettyOutput(PrettyOutputBase): + + stdout = Stream(sys.stdout, ctypes.windll.kernel32.GetStdHandle(-11)) + stderr = Stream(sys.stderr, ctypes.windll.kernel32.GetStdHandle(-12)) + + def __enter__(self): + info = _CONSOLE_SCREEN_BUFFER_INFO() + + for s in (PrettyOutput.stdout, PrettyOutput.stderr): + ctypes.windll.kernel32.GetConsoleScreenBufferInfo( + s.os, ctypes.byref(info)) + s.orig_color = info.wAttributes + + return self + + def __exit__(self, *args): + self._restore_orig_color(PrettyOutput.stdout) + self._restore_orig_color(PrettyOutput.stderr) + + def _restore_orig_color(self, stream, lock=_lock): + if not stream.color_enabled: + return + + with lock: + stream = self._set_valid_stream(stream) + self.flush(stream) + if stream.orig_color: + ctypes.windll.kernel32.SetConsoleTextAttribute( + stream.os, stream.orig_color) + + def _color(self, text, color, stream, lock=_lock): + stream = self._set_valid_stream(stream) + with lock: + try: + if stream.color_enabled: + ctypes.windll.kernel32.SetConsoleTextAttribute( + stream.os, color) + self._write(text, stream) + finally: + if stream.color_enabled: + self._restore_orig_color(stream, lock=_null_lock) + + def red_impl(self, text, stream=None, **kwargs): + self._color(text, 12, stream, **kwargs) + + def yellow_impl(self, text, stream=None, **kwargs): + self._color(text, 14, stream, **kwargs) + + def green_impl(self, text, stream=None, **kwargs): + self._color(text, 10, stream, **kwargs) + + def blue_impl(self, text, stream=None, **kwargs): + self._color(text, 11, stream, **kwargs) + + def default_impl(self, text, stream=None, **kwargs): + stream = self._set_valid_stream(stream) + self._color(text, stream.orig_color, stream, **kwargs) diff --git a/debuginfo-tests/dexter/dex/utils/windows/__init__.py b/debuginfo-tests/dexter/dex/utils/windows/__init__.py new file mode 100644 index 00000000000..1194affd891 --- /dev/null +++ b/debuginfo-tests/dexter/dex/utils/windows/__init__.py @@ -0,0 +1,6 @@ +# 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 |