summaryrefslogtreecommitdiffstats
path: root/debuginfo-tests/dexter/dex
diff options
context:
space:
mode:
authorJeremy Morse <jeremy.morse@sony.com>2019-10-31 16:51:53 +0000
committerJeremy Morse <jeremy.morse@sony.com>2019-10-31 16:51:53 +0000
commit984fad243d179564df31c5f9531a52442e24581a (patch)
treeaba85a27f1596d456079f6f5eb69e09408730b49 /debuginfo-tests/dexter/dex
parent34f3c0fc44a5fd8a0f9186002749336e398837cf (diff)
downloadbcm5719-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')
-rw-r--r--debuginfo-tests/dexter/dex/__init__.py8
-rw-r--r--debuginfo-tests/dexter/dex/builder/Builder.py117
-rw-r--r--debuginfo-tests/dexter/dex/builder/ParserOptions.py56
-rw-r--r--debuginfo-tests/dexter/dex/builder/__init__.py10
-rwxr-xr-xdebuginfo-tests/dexter/dex/builder/scripts/posix/clang-c.sh16
-rwxr-xr-xdebuginfo-tests/dexter/dex/builder/scripts/posix/clang.sh16
-rw-r--r--debuginfo-tests/dexter/dex/builder/scripts/windows/clang-cl_vs2015.bat23
-rw-r--r--debuginfo-tests/dexter/dex/builder/scripts/windows/clang.bat17
-rw-r--r--debuginfo-tests/dexter/dex/command/CommandBase.py54
-rw-r--r--debuginfo-tests/dexter/dex/command/ParseCommand.py421
-rw-r--r--debuginfo-tests/dexter/dex/command/StepValueInfo.py23
-rw-r--r--debuginfo-tests/dexter/dex/command/__init__.py9
-rw-r--r--debuginfo-tests/dexter/dex/command/commands/DexExpectProgramState.py83
-rw-r--r--debuginfo-tests/dexter/dex/command/commands/DexExpectStepKind.py45
-rw-r--r--debuginfo-tests/dexter/dex/command/commands/DexExpectStepOrder.py39
-rw-r--r--debuginfo-tests/dexter/dex/command/commands/DexExpectWatchBase.py197
-rw-r--r--debuginfo-tests/dexter/dex/command/commands/DexExpectWatchType.py26
-rw-r--r--debuginfo-tests/dexter/dex/command/commands/DexExpectWatchValue.py27
-rw-r--r--debuginfo-tests/dexter/dex/command/commands/DexLabel.py31
-rw-r--r--debuginfo-tests/dexter/dex/command/commands/DexUnreachable.py38
-rw-r--r--debuginfo-tests/dexter/dex/command/commands/DexWatch.py39
-rw-r--r--debuginfo-tests/dexter/dex/debugger/DebuggerBase.py227
-rw-r--r--debuginfo-tests/dexter/dex/debugger/Debuggers.py299
-rw-r--r--debuginfo-tests/dexter/dex/debugger/__init__.py8
-rw-r--r--debuginfo-tests/dexter/dex/debugger/dbgeng/README.md60
-rw-r--r--debuginfo-tests/dexter/dex/debugger/dbgeng/__init__.py19
-rw-r--r--debuginfo-tests/dexter/dex/debugger/dbgeng/breakpoint.py88
-rw-r--r--debuginfo-tests/dexter/dex/debugger/dbgeng/client.py185
-rw-r--r--debuginfo-tests/dexter/dex/debugger/dbgeng/control.py405
-rw-r--r--debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py163
-rw-r--r--debuginfo-tests/dexter/dex/debugger/dbgeng/probe_process.py80
-rw-r--r--debuginfo-tests/dexter/dex/debugger/dbgeng/setup.py185
-rw-r--r--debuginfo-tests/dexter/dex/debugger/dbgeng/symbols.py499
-rw-r--r--debuginfo-tests/dexter/dex/debugger/dbgeng/symgroup.py98
-rw-r--r--debuginfo-tests/dexter/dex/debugger/dbgeng/sysobjs.py200
-rw-r--r--debuginfo-tests/dexter/dex/debugger/dbgeng/utils.py47
-rw-r--r--debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py244
-rw-r--r--debuginfo-tests/dexter/dex/debugger/lldb/__init__.py8
-rw-r--r--debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py224
-rw-r--r--debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio2015.py23
-rw-r--r--debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio2017.py23
-rw-r--r--debuginfo-tests/dexter/dex/debugger/visualstudio/__init__.py9
-rw-r--r--debuginfo-tests/dexter/dex/debugger/visualstudio/windows/ComInterface.py119
-rw-r--r--debuginfo-tests/dexter/dex/debugger/visualstudio/windows/__init__.py6
-rw-r--r--debuginfo-tests/dexter/dex/dextIR/BuilderIR.py16
-rw-r--r--debuginfo-tests/dexter/dex/dextIR/DebuggerIR.py14
-rw-r--r--debuginfo-tests/dexter/dex/dextIR/DextIR.py129
-rw-r--r--debuginfo-tests/dexter/dex/dextIR/FrameIR.py16
-rw-r--r--debuginfo-tests/dexter/dex/dextIR/LocIR.py45
-rw-r--r--debuginfo-tests/dexter/dex/dextIR/ProgramState.py117
-rw-r--r--debuginfo-tests/dexter/dex/dextIR/StepIR.py103
-rw-r--r--debuginfo-tests/dexter/dex/dextIR/ValueIR.py38
-rw-r--r--debuginfo-tests/dexter/dex/dextIR/__init__.py17
-rw-r--r--debuginfo-tests/dexter/dex/heuristic/Heuristic.py497
-rw-r--r--debuginfo-tests/dexter/dex/heuristic/__init__.py8
-rw-r--r--debuginfo-tests/dexter/dex/tools/Main.py207
-rw-r--r--debuginfo-tests/dexter/dex/tools/TestToolBase.py148
-rw-r--r--debuginfo-tests/dexter/dex/tools/ToolBase.py135
-rw-r--r--debuginfo-tests/dexter/dex/tools/__init__.py10
-rw-r--r--debuginfo-tests/dexter/dex/tools/clang_opt_bisect/Tool.py286
-rw-r--r--debuginfo-tests/dexter/dex/tools/clang_opt_bisect/__init__.py8
-rw-r--r--debuginfo-tests/dexter/dex/tools/help/Tool.py61
-rw-r--r--debuginfo-tests/dexter/dex/tools/help/__init__.py8
-rw-r--r--debuginfo-tests/dexter/dex/tools/list_debuggers/Tool.py40
-rw-r--r--debuginfo-tests/dexter/dex/tools/list_debuggers/__init__.py8
-rw-r--r--debuginfo-tests/dexter/dex/tools/no_tool_/Tool.py49
-rw-r--r--debuginfo-tests/dexter/dex/tools/no_tool_/__init__.py8
-rw-r--r--debuginfo-tests/dexter/dex/tools/run_debugger_internal_/Tool.py74
-rw-r--r--debuginfo-tests/dexter/dex/tools/run_debugger_internal_/__init__.py8
-rw-r--r--debuginfo-tests/dexter/dex/tools/test/Tool.py244
-rw-r--r--debuginfo-tests/dexter/dex/tools/test/__init__.py8
-rw-r--r--debuginfo-tests/dexter/dex/tools/view/Tool.py59
-rw-r--r--debuginfo-tests/dexter/dex/tools/view/__init__.py8
-rw-r--r--debuginfo-tests/dexter/dex/utils/Environment.py22
-rw-r--r--debuginfo-tests/dexter/dex/utils/Exceptions.py72
-rw-r--r--debuginfo-tests/dexter/dex/utils/ExtArgParse.py148
-rw-r--r--debuginfo-tests/dexter/dex/utils/PrettyOutputBase.py392
-rw-r--r--debuginfo-tests/dexter/dex/utils/ReturnCode.py20
-rw-r--r--debuginfo-tests/dexter/dex/utils/RootDirectory.py15
-rw-r--r--debuginfo-tests/dexter/dex/utils/Timer.py50
-rw-r--r--debuginfo-tests/dexter/dex/utils/UnitTests.py62
-rw-r--r--debuginfo-tests/dexter/dex/utils/Version.py40
-rw-r--r--debuginfo-tests/dexter/dex/utils/Warning.py18
-rw-r--r--debuginfo-tests/dexter/dex/utils/WorkingDirectory.py46
-rw-r--r--debuginfo-tests/dexter/dex/utils/__init__.py21
-rw-r--r--debuginfo-tests/dexter/dex/utils/posix/PrettyOutput.py34
-rw-r--r--debuginfo-tests/dexter/dex/utils/posix/__init__.py6
-rw-r--r--debuginfo-tests/dexter/dex/utils/windows/PrettyOutput.py83
-rw-r--r--debuginfo-tests/dexter/dex/utils/windows/__init__.py6
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
OpenPOWER on IntegriCloud