summaryrefslogtreecommitdiffstats
path: root/debuginfo-tests/dexter/dex
diff options
context:
space:
mode:
authorJeremy Morse <jeremy.morse@sony.com>2019-10-31 13:41:24 +0000
committerJeremy Morse <jeremy.morse@sony.com>2019-10-31 13:49:47 +0000
commitf78c236efda85af1e526ac35ed535ef4786450e3 (patch)
tree1ff1609178e085f58b46dfcbce21fd6b2ef40025 /debuginfo-tests/dexter/dex
parentefacf2ce55d698e5df8173f0d4dacbc7d3c7fd34 (diff)
downloadbcm5719-llvm-f78c236efda85af1e526ac35ed535ef4786450e3.tar.gz
bcm5719-llvm-f78c236efda85af1e526ac35ed535ef4786450e3.zip
Import Dexter to debuginfo-tests
Dexter (Debug Experience Tester) is a test-driver for our debug info integration tests, reading a set of debug experience expectations and comparing them with the actual behaviour of a program under a debugger. More about Dexter can be found in the RFC: http://lists.llvm.org/pipermail/llvm-dev/2019-October/135773.html and the phab review in D68708. Not all the debuginfo tests have been transformed into Dexter tests, and we look forwards to doing that incrementally. This commit mostly aims to flush out buildbots that are running debuginfo-tests but don't have python 3 installed, possibly green-dragon and some windows bots.
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