Reapply "Import Dexter to debuginfo-tests""
authorJeremy Morse <jeremy.morse@sony.com>
Thu, 31 Oct 2019 16:51:53 +0000 (16:51 +0000)
committerJeremy Morse <jeremy.morse@sony.com>
Thu, 31 Oct 2019 16:51:53 +0000 (16:51 +0000)
This reverts commit cb935f345683194e42e6e883d79c5a16479acd74.

Discussion in D68708 advises that green dragon is being briskly
refurbished, and it's good to have this patch up testing it.

166 files changed:
debuginfo-tests/CMakeLists.txt
debuginfo-tests/README.txt
debuginfo-tests/aggregate-indirect-arg.cpp [deleted file]
debuginfo-tests/ctor.cpp [deleted file]
debuginfo-tests/dexter-tests/aggregate-indirect-arg.cpp [new file with mode: 0644]
debuginfo-tests/dexter-tests/asan-deque.cpp [new file with mode: 0644]
debuginfo-tests/dexter-tests/asan.c [new file with mode: 0644]
debuginfo-tests/dexter-tests/ctor.cpp [new file with mode: 0644]
debuginfo-tests/dexter-tests/dbg-arg.c [moved from debuginfo-tests/dbg-arg.c with 55% similarity]
debuginfo-tests/dexter-tests/global-constant.cpp [new file with mode: 0644]
debuginfo-tests/dexter-tests/hello.c [new file with mode: 0644]
debuginfo-tests/dexter-tests/inline-line-gap.cpp [moved from debuginfo-tests/win_cdb/inline-line-gap.cpp with 59% similarity]
debuginfo-tests/dexter-tests/nrvo-string.cpp [new file with mode: 0644]
debuginfo-tests/dexter-tests/nrvo.cpp [moved from debuginfo-tests/win_cdb/nrvo.cpp with 57% similarity]
debuginfo-tests/dexter-tests/realigned-frame.cpp [new file with mode: 0644]
debuginfo-tests/dexter-tests/stack-var.c [new file with mode: 0644]
debuginfo-tests/dexter-tests/vla.c [new file with mode: 0644]
debuginfo-tests/dexter/.gitignore [new file with mode: 0644]
debuginfo-tests/dexter/Commands.md [new file with mode: 0644]
debuginfo-tests/dexter/LICENSE.txt [new file with mode: 0644]
debuginfo-tests/dexter/README.md [new file with mode: 0644]
debuginfo-tests/dexter/dex/__init__.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/builder/Builder.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/builder/ParserOptions.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/builder/__init__.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/builder/scripts/posix/clang-c.sh [new file with mode: 0755]
debuginfo-tests/dexter/dex/builder/scripts/posix/clang.sh [new file with mode: 0755]
debuginfo-tests/dexter/dex/builder/scripts/windows/clang-cl_vs2015.bat [new file with mode: 0644]
debuginfo-tests/dexter/dex/builder/scripts/windows/clang.bat [new file with mode: 0644]
debuginfo-tests/dexter/dex/command/CommandBase.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/command/ParseCommand.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/command/StepValueInfo.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/command/__init__.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/command/commands/DexExpectProgramState.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/command/commands/DexExpectStepKind.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/command/commands/DexExpectStepOrder.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/command/commands/DexExpectWatchBase.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/command/commands/DexExpectWatchType.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/command/commands/DexExpectWatchValue.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/command/commands/DexLabel.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/command/commands/DexUnreachable.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/command/commands/DexWatch.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/debugger/DebuggerBase.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/debugger/Debuggers.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/debugger/__init__.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/debugger/dbgeng/README.md [new file with mode: 0644]
debuginfo-tests/dexter/dex/debugger/dbgeng/__init__.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/debugger/dbgeng/breakpoint.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/debugger/dbgeng/client.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/debugger/dbgeng/control.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/debugger/dbgeng/dbgeng.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/debugger/dbgeng/probe_process.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/debugger/dbgeng/setup.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/debugger/dbgeng/symbols.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/debugger/dbgeng/symgroup.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/debugger/dbgeng/sysobjs.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/debugger/dbgeng/utils.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/debugger/lldb/LLDB.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/debugger/lldb/__init__.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio2015.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/debugger/visualstudio/VisualStudio2017.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/debugger/visualstudio/__init__.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/debugger/visualstudio/windows/ComInterface.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/debugger/visualstudio/windows/__init__.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/dextIR/BuilderIR.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/dextIR/DebuggerIR.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/dextIR/DextIR.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/dextIR/FrameIR.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/dextIR/LocIR.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/dextIR/ProgramState.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/dextIR/StepIR.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/dextIR/ValueIR.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/dextIR/__init__.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/heuristic/Heuristic.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/heuristic/__init__.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/tools/Main.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/tools/TestToolBase.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/tools/ToolBase.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/tools/__init__.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/tools/clang_opt_bisect/Tool.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/tools/clang_opt_bisect/__init__.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/tools/help/Tool.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/tools/help/__init__.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/tools/list_debuggers/Tool.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/tools/list_debuggers/__init__.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/tools/no_tool_/Tool.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/tools/no_tool_/__init__.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/tools/run_debugger_internal_/Tool.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/tools/run_debugger_internal_/__init__.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/tools/test/Tool.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/tools/test/__init__.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/tools/view/Tool.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/tools/view/__init__.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/utils/Environment.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/utils/Exceptions.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/utils/ExtArgParse.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/utils/PrettyOutputBase.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/utils/ReturnCode.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/utils/RootDirectory.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/utils/Timer.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/utils/UnitTests.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/utils/Version.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/utils/Warning.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/utils/WorkingDirectory.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/utils/__init__.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/utils/posix/PrettyOutput.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/utils/posix/__init__.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/utils/windows/PrettyOutput.py [new file with mode: 0644]
debuginfo-tests/dexter/dex/utils/windows/__init__.py [new file with mode: 0644]
debuginfo-tests/dexter/dexter.py [new file with mode: 0755]
debuginfo-tests/dexter/feature_tests/Readme.md [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/commands/penalty/expect_program_state.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/commands/penalty/expect_step_kinds.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/commands/penalty/expect_step_order.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/commands/penalty/expect_watch_type.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/commands/penalty/expect_watch_value.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/commands/penalty/unreachable.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/commands/perfect/expect_program_state.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/commands/perfect/expect_step_kind/direction.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/commands/perfect/expect_step_kind/func.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/commands/perfect/expect_step_kind/func_external.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/commands/perfect/expect_step_kind/recursive.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/commands/perfect/expect_step_kind/small_loop.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/commands/perfect/expect_step_order.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/commands/perfect/expect_watch_type.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/commands/perfect/expect_watch_value.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/commands/perfect/unreachable.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/lit.local.cfg [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/subtools/clang-opt-bisect/clang-opt-bisect.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/subtools/help/help.test [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/subtools/list-debuggers/list-debuggers.test [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/subtools/test/err_paren.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/subtools/test/err_paren_mline.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/subtools/test/err_syntax.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/subtools/test/err_syntax_mline.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/subtools/test/err_type.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/subtools/test/err_type_mline.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/subtools/view.cpp [new file with mode: 0644]
debuginfo-tests/dexter/feature_tests/unittests/run.test [new file with mode: 0644]
debuginfo-tests/lit.cfg.py
debuginfo-tests/lit.site.cfg.py.in
debuginfo-tests/llgdb-tests/apple-accel.cpp [moved from debuginfo-tests/apple-accel.cpp with 100% similarity]
debuginfo-tests/llgdb-tests/asan-blocks.c [moved from debuginfo-tests/asan-blocks.c with 96% similarity]
debuginfo-tests/llgdb-tests/asan-deque.cpp [moved from debuginfo-tests/asan-deque.cpp with 97% similarity]
debuginfo-tests/llgdb-tests/asan.c [moved from debuginfo-tests/asan.c with 96% similarity]
debuginfo-tests/llgdb-tests/block_var.m [moved from debuginfo-tests/block_var.m with 100% similarity]
debuginfo-tests/llgdb-tests/blocks.m [moved from debuginfo-tests/blocks.m with 100% similarity]
debuginfo-tests/llgdb-tests/foreach.m [moved from debuginfo-tests/foreach.m with 100% similarity]
debuginfo-tests/llgdb-tests/forward-declare-class.cpp [moved from debuginfo-tests/forward-declare-class.cpp with 100% similarity]
debuginfo-tests/llgdb-tests/lit.local.cfg [moved from debuginfo-tests/lit.local.cfg with 60% similarity]
debuginfo-tests/llgdb-tests/llgdb.py [moved from debuginfo-tests/llgdb.py with 100% similarity, mode: 0755]
debuginfo-tests/llgdb-tests/nested-struct.cpp [moved from debuginfo-tests/nested-struct.cpp with 100% similarity]
debuginfo-tests/llgdb-tests/nrvo-string.cpp [moved from debuginfo-tests/nrvo-string.cpp with 100% similarity]
debuginfo-tests/llgdb-tests/safestack.c [moved from debuginfo-tests/safestack.c with 97% similarity]
debuginfo-tests/llgdb-tests/static-member-2.cpp [moved from debuginfo-tests/static-member-2.cpp with 100% similarity]
debuginfo-tests/llgdb-tests/static-member.cpp [moved from debuginfo-tests/static-member.cpp with 100% similarity]
debuginfo-tests/llgdb-tests/test_debuginfo.pl [moved from debuginfo-tests/test_debuginfo.pl with 100% similarity]
debuginfo-tests/sret.cpp [deleted file]
debuginfo-tests/stack-var.c [deleted file]
debuginfo-tests/vla.c [deleted file]
debuginfo-tests/win_cdb-tests/README.txt [moved from debuginfo-tests/win_cdb/README.txt with 60% similarity]
debuginfo-tests/win_cdb-tests/lit.local.cfg.py [moved from debuginfo-tests/win_cdb/lit.local.cfg.py with 100% similarity]
debuginfo-tests/win_cdb/global-constant.cpp [deleted file]
debuginfo-tests/win_cdb/hello.c [deleted file]
debuginfo-tests/win_cdb/realigned-frame.cpp [deleted file]

index fbab61c..0ac48da 100644 (file)
@@ -13,15 +13,47 @@ set(DEBUGINFO_TEST_DEPS
   not
   )
 
-configure_lit_site_cfg(
-  ${CMAKE_CURRENT_SOURCE_DIR}/lit.site.cfg.py.in
-  ${CMAKE_CURRENT_BINARY_DIR}/lit.site.cfg.py
-  MAIN_CONFIG
-  ${CMAKE_CURRENT_SOURCE_DIR}/lit.cfg.py
-  )
+# Wipe, uh, previous results
+unset(PYTHONINTERP_FOUND CACHE)
+unset(PYTHON_EXECUTABLE CACHE)
+unset(PYTHON_LIBRARY CACHE)
+unset(PYTHON_DLL CACHE)
+unset(PYTHON_INCLUDE_DIR CACHE)
+unset(PYTHON_VERSION_STRING CACHE)
+unset(PYTHON_VERSION_MAJOR CACHE)
+unset(PYTHON_VERSION_MINOR CACHE)
+unset(PYTHON_VERSION_PATCH CACHE)
+unset(PYTHONLIBS_VERSION_STRING CACHE)
 
-add_lit_testsuite(check-debuginfo "Running debug info integration tests"
-  ${CMAKE_CURRENT_BINARY_DIR}
-  DEPENDS ${DEBUGINFO_TEST_DEPS}
-  )
-set_target_properties(check-debuginfo PROPERTIES FOLDER "Debug info tests")
+# Try to find python3. If it doesn't exist, dexter tests can't run.
+find_package(PythonInterp "3")
+if (NOT DEFINED PYTHON_EXECUTABLE)
+  message(FATAL_ERROR "Cannot run debuginfo-tests without python")
+elseif(PYTHON_VERSION_MAJOR LESS 3)
+  message(FATAL_ERROR "Cannot run debuginfo-tests without python 3")
+else()
+   configure_lit_site_cfg(
+    ${CMAKE_CURRENT_SOURCE_DIR}/lit.site.cfg.py.in
+    ${CMAKE_CURRENT_BINARY_DIR}/lit.site.cfg.py
+    MAIN_CONFIG
+    ${CMAKE_CURRENT_SOURCE_DIR}/lit.cfg.py
+    )
+
+  add_lit_testsuite(check-debuginfo "Running debug info integration tests"
+    ${CMAKE_CURRENT_BINARY_DIR}
+    DEPENDS ${DEBUGINFO_TEST_DEPS}
+    )
+  set_target_properties(check-debuginfo PROPERTIES FOLDER "Debug info tests") 
+endif()
+
+# Prevent the rest of llvm observing our secret python3-ness
+unset(PYTHONINTERP_FOUND CACHE)
+unset(PYTHON_EXECUTABLE CACHE)
+unset(PYTHON_LIBRARY CACHE)
+unset(PYTHON_DLL CACHE)
+unset(PYTHON_INCLUDE_DIR CACHE)
+unset(PYTHON_VERSION_STRING CACHE)
+unset(PYTHON_VERSION_MAJOR CACHE)
+unset(PYTHON_VERSION_MINOR CACHE)
+unset(PYTHON_VERSION_PATCH CACHE)
+unset(PYTHONLIBS_VERSION_STRING CACHE)
index 0c56d29..544e6ff 100644 (file)
@@ -1,9 +1,11 @@
                                                                    -*- rst -*-
 This is a collection of tests to check debugging information generated by 
 compiler. This test suite can be checked out inside clang/test folder. This 
-will enable 'make test' for clang to pick up these tests. Typically, test 
-cases included here includes debugger commands and intended debugger output 
-as comments in source file using DEBUGGER: and CHECK: as prefixes respectively.
+will enable 'make test' for clang to pick up these tests.
+
+Some tests (in the 'llgdb-tests' directory) are written with debugger
+commands and checks for the intended debugger output in the source file,
+using DEBUGGER: and CHECK: as prefixes respectively.
 
 For example::
 
@@ -17,3 +19,25 @@ For example::
 
 is a testcase where the debugger is asked to break at function 'f1' and 
 print value of argument 'i'. The expected value of 'i' is 42 in this case.
+
+Other tests are written for use with the 'Dexter' tool (in the 'dexter-tests'
+and 'dexter' directories respectively). These use a domain specific language
+in comments to describe the intended debugger experience in a more abstract
+way than debugger commands. This allows for testing integration across
+multiple debuggers from one input language.
+
+For example::
+
+  void __attribute__((noinline, optnone)) bar(int *test) {}
+  int main() {
+    int test;
+    test = 23;
+    bar(&test); // DexLabel('before_bar')
+    return test; // DexLabel('after_bar')
+  }
+
+  // DexExpectWatchValue('test', '23', on_line='before_bar')
+  // DexExpectWatchValue('test', '23', on_line='after_bar')
+
+Labels two lines with the names 'before_bar' and 'after_bar', and records that
+the 'test' variable is expected to have the value 23 on both of them.
diff --git a/debuginfo-tests/aggregate-indirect-arg.cpp b/debuginfo-tests/aggregate-indirect-arg.cpp
deleted file mode 100644 (file)
index 86c7caf..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-// RUN: %clangxx %target_itanium_abi_host_triple -O0 -g %s -c -o %t.o
-// RUN: %clangxx %target_itanium_abi_host_triple %t.o -o %t.out
-// RUN: %test_debuginfo %s %t.out 
-// Radar 8945514
-// DEBUGGER: break 22
-// DEBUGGER: r
-// DEBUGGER: p v
-// CHECK: ${{[0-9]+}} =
-// CHECK:  Data ={{.*}} 0x0{{(0*)}}
-// CHECK:  Kind = 2142
-
-class SVal {
-public:
-  ~SVal() {}
-  const void* Data;
-  unsigned Kind;
-};
-
-void bar(SVal &v) {}
-class A {
-public:
-  void foo(SVal v) { bar(v); }
-};
-
-int main() {
-  SVal v;
-  v.Data = 0;
-  v.Kind = 2142;
-  A a;
-  a.foo(v);
-  return 0;
-}
diff --git a/debuginfo-tests/ctor.cpp b/debuginfo-tests/ctor.cpp
deleted file mode 100644 (file)
index 92cdbcd..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-// RUN: %clangxx %target_itanium_abi_host_triple -O0 -g %s -c -o %t.o
-// RUN: %clangxx %target_itanium_abi_host_triple %t.o -o %t.out
-// RUN: %test_debuginfo %s %t.out 
-
-
-// DEBUGGER: break 14
-// DEBUGGER: r
-// DEBUGGER: p *this
-// CHECK-NEXT-NOT: Cannot access memory at address 
-
-class A {
-public:
-       A() : zero(0), data(42)
-       {
-       }
-private:
-       int zero;
-       int data;
-};
-
-int main() {
-       A a;
-       return 0;
-}
-
diff --git a/debuginfo-tests/dexter-tests/aggregate-indirect-arg.cpp b/debuginfo-tests/dexter-tests/aggregate-indirect-arg.cpp
new file mode 100644 (file)
index 0000000..4c495c9
--- /dev/null
@@ -0,0 +1,43 @@
+// REQUIRES: system-linux, lldb
+//
+// RUN: %dexter --fail-lt 1.0 -w \
+// RUN:     --builder 'clang' --debugger 'lldb' --cflags "-O0 -g" \
+// RUN:     --ldflags="-lstdc++" -- %s
+// Radar 8945514
+
+class SVal {
+public:
+  ~SVal() {}
+  const void* Data;
+  unsigned Kind;
+};
+
+void bar(SVal &v) {}
+class A {
+public:
+  void foo(SVal v) { bar(v); } // DexLabel('foo')
+};
+
+int main() {
+  SVal v;
+  v.Data = 0;
+  v.Kind = 2142;
+  A a;
+  a.foo(v);
+  return 0;
+}
+
+/*
+DexExpectProgramState({
+  'frames': [
+    {
+      'location': { 'lineno': 'foo' },
+      'watches': {
+        'v.Data == 0': 'true',
+        'v.Kind': '2142'
+      }
+    }
+  ]
+})
+*/
+
diff --git a/debuginfo-tests/dexter-tests/asan-deque.cpp b/debuginfo-tests/dexter-tests/asan-deque.cpp
new file mode 100644 (file)
index 0000000..50fe167
--- /dev/null
@@ -0,0 +1,47 @@
+// REQUIRES: !asan, system-linux, lldb
+//           Zorg configures the ASAN stage2 bots to not build the asan
+//           compiler-rt. Only run this test on non-asanified configurations.
+// UNSUPPORTED: apple-lldb-pre-1000
+
+// XFAIL: lldb
+// lldb-8, even outside of dexter, will sometimes trigger an asan fault in
+// the debugged process and generally freak out.
+
+// RUN: %dexter --fail-lt 1.0 -w \
+// RUN:     --builder 'clang' --debugger 'lldb' \
+// RUN:     --cflags "-O1 -glldb -fsanitize=address -arch x86_64" \
+// RUN:     --ldflags="-fsanitize=address" -- %s
+#include <deque>
+
+struct A {
+  int a;
+  A(int a) : a(a) {}
+  A() : a(0) {}
+};
+
+using deq_t = std::deque<A>;
+
+template class std::deque<A>;
+
+static void __attribute__((noinline, optnone)) escape(deq_t &deq) {
+  static volatile deq_t *sink;
+  sink = &deq;
+}
+
+int main() {
+  deq_t deq;
+  deq.push_back(1234);
+  deq.push_back(56789);
+  escape(deq); // DexLabel('first')
+  while (!deq.empty()) {
+    auto record = deq.front();
+    deq.pop_front();
+    escape(deq); // DexLabel('second')
+  }
+}
+
+// DexExpectWatchValue('deq[0].a', '1234', on_line='first')
+// DexExpectWatchValue('deq[1].a', '56789', on_line='first')
+
+// DexExpectWatchValue('deq[0].a', '56789', '0', on_line='second')
+
diff --git a/debuginfo-tests/dexter-tests/asan.c b/debuginfo-tests/dexter-tests/asan.c
new file mode 100644 (file)
index 0000000..c7d5263
--- /dev/null
@@ -0,0 +1,28 @@
+// REQUIRES: !asan, system-linux, lldb
+//           Zorg configures the ASAN stage2 bots to not build the asan
+//           compiler-rt. Only run this test on non-asanified configurations.
+//
+// RUN: %dexter --fail-lt 1.0 -w \
+// RUN:     --builder 'clang-c' --debugger 'lldb' \
+// RUN:     --cflags "--driver-mode=gcc -O0 -glldb -fblocks -arch x86_64 \
+// RUN:     -fsanitize=address" --ldflags="-fsanitize=address" -- %s
+
+struct S {
+  int a[8];
+};
+
+int f(struct S s, unsigned i) {
+  return s.a[i]; // DexLabel('asan')
+}
+
+int main(int argc, const char **argv) {
+  struct S s = {{0, 1, 2, 3, 4, 5, 6, 7}};
+  if (f(s, 4) == 4)
+    return f(s, 0);
+  return 0;
+}
+
+// DexExpectWatchValue('s.a[0]', '0', on_line='asan')
+// DexExpectWatchValue('s.a[1]', '1', on_line='asan')
+// DexExpectWatchValue('s.a[7]', '7', on_line='asan')
+
diff --git a/debuginfo-tests/dexter-tests/ctor.cpp b/debuginfo-tests/dexter-tests/ctor.cpp
new file mode 100644 (file)
index 0000000..77195d7
--- /dev/null
@@ -0,0 +1,35 @@
+// REQUIRES: system-linux, lldb
+//
+// RUN: %dexter --fail-lt 1.0 -w \
+// RUN:     --builder 'clang' --debugger 'lldb' --cflags "-O0 -glldb" -- %s
+
+class A {
+public:
+       A() : zero(0), data(42) { // DexLabel('ctor_start')
+       }
+private:
+       int zero;
+       int data;
+};
+
+int main() {
+       A a;
+       return 0;
+}
+
+
+/*
+DexExpectProgramState({
+       'frames': [
+               {
+                       'location': {
+                               'lineno': 'ctor_start'
+                       },
+                       'watches': {
+                               '*this': {'is_irretrievable': False}
+                       }
+               }
+       ]
+})
+*/
+
similarity index 55%
rename from debuginfo-tests/dbg-arg.c
rename to debuginfo-tests/dexter-tests/dbg-arg.c
index a65dc91..7d0ef7b 100644 (file)
@@ -1,12 +1,9 @@
-// This test case checks debug info during register moves for an argument.
-// RUN: %clang %target_itanium_abi_host_triple -m64 -mllvm -fast-isel=false  %s -c -o %t.o -g
-// RUN: %clang %target_itanium_abi_host_triple -m64 %t.o -o %t.out
-// RUN: %test_debuginfo %s %t.out
+// REQUIRES: system-linux, lldb
 //
-// DEBUGGER: break 26
-// DEBUGGER: r
-// DEBUGGER: print mutex
-// CHECK:  ={{.* 0x[0-9A-Fa-f]+}}
+// This test case checks debug info during register moves for an argument.
+// RUN: %dexter --fail-lt 1.0 -w \
+// RUN:     --builder clang-c --debugger 'lldb' \
+// RUN:     --cflags "-m64 -mllvm -fast-isel=false -g" -- %s
 //
 // Radar 8412415
 
@@ -23,7 +20,7 @@ struct _mtx
 
 int foobar(struct _mtx *mutex) {
   int r = 1;
-  int l = 0;
+  int l = 0; // DexLabel('l_assign')
   int j = 0;
   do {
     if (mutex->waiters) {
@@ -44,3 +41,18 @@ int main() {
   m.waiters = 0;
   return foobar(&m);
 }
+
+
+/*
+DexExpectProgramState({
+  'frames': [
+    {
+      'location': { 'lineno': 'l_assign' },
+      'watches': {
+        '*mutex': { 'is_irretrievable': False }
+      }
+    }
+  ]
+})
+*/
+
diff --git a/debuginfo-tests/dexter-tests/global-constant.cpp b/debuginfo-tests/dexter-tests/global-constant.cpp
new file mode 100644 (file)
index 0000000..faad72a
--- /dev/null
@@ -0,0 +1,30 @@
+// REQUIRES: system-windows
+//
+// RUN: %dexter --fail-lt 1.0 -w --builder 'clang-cl_vs2015' \
+// RUN:      --debugger 'dbgeng' --cflags '/Z7 /Zi' --ldflags '/Z7 /Zi' -- %s
+
+// Check that global constants have debug info.
+
+const float TestPi = 3.14;
+struct S {
+  static const char TestCharA = 'a';
+};
+enum TestEnum : int {
+  ENUM_POS = 2147000000,
+  ENUM_NEG = -2147000000,
+};
+void useConst(int) {}
+int main() {
+  useConst(TestPi);
+  useConst(S::TestCharA);
+  useConst(ENUM_NEG); // DexLabel('stop')
+  return 0;
+}
+
+// DexExpectWatchValue('TestPi', 3.140000104904175, on_line='stop')
+// DexExpectWatchValue('S::TestCharA', 97, on_line='stop')
+// DexExpectWatchValue('ENUM_NEG', -2147000000, on_line='stop')
+/* DexExpectProgramState({'frames': [{
+               'location': {'lineno' : 'stop'},
+               'watches': {'ENUM_POS' : {'is_irretrievable': True}}
+}]}) */
diff --git a/debuginfo-tests/dexter-tests/hello.c b/debuginfo-tests/dexter-tests/hello.c
new file mode 100644 (file)
index 0000000..bd94646
--- /dev/null
@@ -0,0 +1,13 @@
+// REQUIRES: system-windows
+//
+// RUN: %dexter --fail-lt 1.0 -w --builder 'clang-cl_vs2015' \
+// RUN:      --debugger 'dbgeng' --cflags '/Z7 /Zi' --ldflags '/Z7 /Zi' -- %s
+
+#include <stdio.h>
+int main() {
+  printf("hello world\n");
+  int x = 42;
+  __debugbreak(); // DexLabel('stop')
+}
+
+// DexExpectWatchValue('x', 42, on_line='stop')
@@ -1,10 +1,12 @@
-// RUN: %clang_cl -MD -Od %s -o %t.exe -fuse-ld=lld -Z7
-// RUN: grep DE[B]UGGER: %s | sed -e 's/.*DE[B]UGGER: //' > %t.script
-// RUN: %cdb -cf %t.script %t.exe | FileCheck %s --check-prefixes=DEBUGGER,CHECK
+// REQUIRES: system-windows
 //
-// RUN: %clang_cl -MD -O2 %s -o %t.exe -fuse-ld=lld -Z7
-// RUN: grep DE[B]UGGER: %s | sed -e 's/.*DE[B]UGGER: //' > %t.script
-// RUN: %cdb -cf %t.script %t.exe | FileCheck %s --check-prefixes=DEBUGGER,CHECK
+// RUN: %dexter --fail-lt 1.0 -w --builder 'clang-cl_vs2015' \
+// RUN:      --debugger 'dbgeng' --cflags '/Od /Z7 /Zi' \
+// RUN:      --ldflags '/Od /Z7 /Zi' -- %s
+//
+// RUN: %dexter --fail-lt 1.0 -w --builder 'clang-cl_vs2015' \
+// RUN:      --debugger 'dbgeng' --cflags '/O2 /Z7 /Zi' \
+// RUN:      --ldflags '/O2 /Z7 /Zi' -- %s
 
 // This code is structured to have an early exit with an epilogue in the middle
 // of the function, which creates a gap between the beginning of the inlined
@@ -20,9 +22,8 @@ extern "C" void __declspec(noreturn) abort();
 void __forceinline inlineCrashFrame() {
   if (shutting_down_ || tearing_down_) {
     setCrashString("crashing");
-    __debugbreak();
     // MSVC lays out calls to abort out of line, gets the layout we want.
-    abort();
+    abort(); // DexLabel('stop')
   }
 }
 
@@ -37,9 +38,10 @@ int __attribute__((optnone)) main() {
   callerOfInlineCrashFrame(true);
 }
 
-// DEBUGGER: g
-// DEBUGGER: k3
-// CHECK: {{.*}}!inlineCrashFrame
-// CHECK: {{.*}}!callerOfInlineCrashFrame
-// CHECK: {{.*}}!main
-// DEBUGGER: q
+/*
+DexExpectProgramState({'frames':[
+     {'function': 'inlineCrashFrame', 'location':{'lineno' : 'stop'} },
+     {'function': 'callerOfInlineCrashFrame'},
+     {'function': 'main'}
+]})
+*/
diff --git a/debuginfo-tests/dexter-tests/nrvo-string.cpp b/debuginfo-tests/dexter-tests/nrvo-string.cpp
new file mode 100644 (file)
index 0000000..79561dc
--- /dev/null
@@ -0,0 +1,55 @@
+// Purpose:
+//     This ensures that DW_OP_deref is inserted when necessary, such as when
+//     NRVO of a string object occurs in C++.
+//
+// REQUIRES: !asan, system-linux, lldb
+//           Zorg configures the ASAN stage2 bots to not build the asan
+//           compiler-rt. Only run this test on non-asanified configurations.
+//
+// RUN: %dexter --fail-lt 1.0 -w \
+// RUN:     --builder 'clang' --debugger 'lldb' \
+// RUN:     --cflags "-O0 -glldb -fno-exceptions" -- %s
+//
+// RUN: %dexter --fail-lt 1.0 -w \
+// RUN:     --builder 'clang' --debugger 'lldb' \
+// RUN:     --cflags "-O1 -glldb -fno-exceptions" -- %s
+//
+// PR34513
+volatile int sideeffect = 0;
+void __attribute__((noinline)) stop() { sideeffect++; }
+
+struct string {
+  string() {}
+  string(int i) : i(i) {}
+  ~string() {}
+  int i = 0;
+};
+string get_string() {
+  string unused;
+  string output = 3;
+  stop(); // DexLabel('string-nrvo')
+  return output;
+}
+void some_function(int) {}
+struct string2 {
+  string2() = default;
+  string2(string2 &&other) { i = other.i; }
+  int i;
+};
+string2 get_string2() {
+  string2 output;
+  output.i = 5;
+  some_function(output.i);
+  // Test that the debugger can get the value of output after another
+  // function is called.
+  stop(); // DexLabel('string2-nrvo')
+  return output;
+}
+int main() {
+  get_string();
+  get_string2();
+}
+
+// DexExpectWatchValue('output.i', 3, on_line='string-nrvo')
+// DexExpectWatchValue('output.i', 5, on_line='string2-nrvo')
+
similarity index 57%
rename from debuginfo-tests/win_cdb/nrvo.cpp
rename to debuginfo-tests/dexter-tests/nrvo.cpp
index 5712118..9ce0197 100644 (file)
@@ -1,10 +1,10 @@
 // This ensures that DW_OP_deref is inserted when necessary, such as when NRVO
 // of a string object occurs in C++.
 //
-// RUN: %clang_cl %s -o %t.exe -fuse-ld=lld -Z7
-// RUN: grep DE[B]UGGER: %s | sed -e 's/.*DE[B]UGGER: //' > %t.script
-// RUN: %cdb -cf %t.script %t.exe | FileCheck %s --check-prefixes=DEBUGGER,CHECK
+// REQUIRES: system-windows
 //
+// RUN: %dexter --fail-lt 1.0 -w --builder 'clang-cl_vs2015' \
+// RUN:      --debugger 'dbgeng' --cflags '/Z7 /Zi' --ldflags '/Z7 /Zi' -- %s
 
 struct string {
   string() {}
@@ -15,8 +15,7 @@ struct string {
 string get_string() {
   string unused;
   string result = 3;
-  __debugbreak();
-  return result;
+  return result; // DexLabel('readresult1')
 }
 void some_function(int) {}
 struct string2 {
@@ -30,20 +29,12 @@ string2 get_string2() {
   some_function(result.i);
   // Test that the debugger can get the value of result after another
   // function is called.
-  __debugbreak();
-  return result;
+  return result; // DexLabel('readresult2')
 }
 int main() {
   get_string();
   get_string2();
 }
 
-// DEBUGGER: g
-// DEBUGGER: ?? result
-// CHECK: struct string *
-// CHECK:    +0x000 i : 0n3
-// DEBUGGER: g
-// DEBUGGER: ?? result
-// CHECK: struct string2 *
-// CHECK:    +0x000 i : 0n5
-// DEBUGGER: q
+// DexExpectWatchValue('result.i', 3, on_line='readresult1')
+// DexExpectWatchValue('result.i', 5, on_line='readresult2')
diff --git a/debuginfo-tests/dexter-tests/realigned-frame.cpp b/debuginfo-tests/dexter-tests/realigned-frame.cpp
new file mode 100644 (file)
index 0000000..3d3c086
--- /dev/null
@@ -0,0 +1,39 @@
+// REQUIRES: system-windows
+//
+// RUN: %dexter --fail-lt 1.0 -w --builder 'clang-cl_vs2015' \
+// RUN:      --debugger 'dbgeng' --cflags '/Z7 /Zi' --ldflags '/Z7 /Zi' -- %s
+
+// From https://llvm.org/pr38857, where we had issues with stack realignment.
+
+struct Foo {
+  int x = 42;
+  int __declspec(noinline) foo();
+  void __declspec(noinline) bar(int *a, int *b, double *c);
+};
+int Foo::foo() {
+  int a = 1;
+  int b = 2;
+  double __declspec(align(32)) force_alignment = 0.42;
+  bar(&a, &b, &force_alignment); // DexLabel('in_foo')
+  x += (int)force_alignment;
+  return x;
+}
+void Foo::bar(int *a, int *b, double *c) {
+  *c += *a + *b; // DexLabel('in_bar')
+}
+int main() {
+  Foo o;
+  o.foo();
+}
+/*
+DexExpectProgramState({'frames':[
+    {'function': 'Foo::bar', 'location' : {'lineno' : 'in_bar'} },
+    {'function': 'Foo::foo',
+     'watches' : {
+       'a' : '1',
+       'b' : '2',
+       'force_alignment' : '0.42'
+     }
+    }
+]})
+*/
diff --git a/debuginfo-tests/dexter-tests/stack-var.c b/debuginfo-tests/dexter-tests/stack-var.c
new file mode 100644 (file)
index 0000000..15321b0
--- /dev/null
@@ -0,0 +1,16 @@
+// REQUIRES: system-linux, lldb
+//
+// RUN: %dexter --fail-lt 1.0 -w \
+// RUN:     --builder clang-c --debugger 'lldb' --cflags "-O -glldb" -- %s
+
+void __attribute__((noinline, optnone)) bar(int *test) {}
+int main() {
+  int test;
+  test = 23;
+  bar(&test); // DexLabel('before_bar')
+  return test; // DexLabel('after_bar')
+}
+
+// DexExpectWatchValue('test', '23', on_line='before_bar')
+// DexExpectWatchValue('test', '23', on_line='after_bar')
+
diff --git a/debuginfo-tests/dexter-tests/vla.c b/debuginfo-tests/dexter-tests/vla.c
new file mode 100644 (file)
index 0000000..a06bf3c
--- /dev/null
@@ -0,0 +1,22 @@
+// This test case verifies the debug location for variable-length arrays.
+// REQUIRES: system-linux, lldb
+//
+// RUN: %dexter --fail-lt 1.0 -w \
+// RUN:     --builder clang-c --debugger 'lldb' --cflags "-O0 -glldb" -- %s
+
+void init_vla(int size) {
+  int i;
+  int vla[size];
+  for (i = 0; i < size; i++)
+    vla[i] = size-i;
+  vla[0] = size; // DexLabel('end_init')
+}
+
+int main(int argc, const char **argv) {
+  init_vla(23);
+  return 0;
+}
+
+// DexExpectWatchValue('vla[0]', '23', on_line='end_init')
+// DexExpectWatchValue('vla[1]', '22', on_line='end_init')
+
diff --git a/debuginfo-tests/dexter/.gitignore b/debuginfo-tests/dexter/.gitignore
new file mode 100644 (file)
index 0000000..042c4e0
--- /dev/null
@@ -0,0 +1,3 @@
+/build/
+/results/
+
diff --git a/debuginfo-tests/dexter/Commands.md b/debuginfo-tests/dexter/Commands.md
new file mode 100644 (file)
index 0000000..c30a0d7
--- /dev/null
@@ -0,0 +1,204 @@
+# Dexter commands
+
+* [DexExpectProgramState](Commands.md#DexExpectProgramState)
+* [DexExpectStepKind](Commands.md#DexExpectStepKind)
+* [DexExpectStepOrder](Commands.md#DexExpectStepOrder)
+* [DexExpectWatchType](Commands.md#DexExpectWatchType)
+* [DexExpectWatchValue](Commands.md#DexExpectWatchValue)
+* [DexUnreachable](Commands.md#DexUnreachable)
+* [DexWatch](Commands.md#DexWatch)
+
+---
+## DexExpectProgramState
+    DexExpectProgramState(state [,**times])
+
+    Args:
+        state (dict): { 'frames': [
+                        {
+                          # StackFrame #
+                          'function': name (str),
+                          'is_inlined': bool,
+                          'location': {
+                            # SourceLocation #
+                            'lineno': int,
+                            'path': str,
+                            'column': int,
+                          },
+                          'watches': {
+                            expr (str): value (str),
+                            expr (str): {
+                              'value': str,
+                              'type_name': str,
+                              'could_evaluate': bool,
+                              'is_optimized_away': bool,
+                              'is_irretrievable': bool,
+                           }
+                          },
+                        }
+                      ]}
+
+    Keyword args:
+        times (int): Minimum number of times this state pattern is expected to
+             be seen. Defaults to 1. Can be 0.
+
+### Description
+Expect to see a given program `state` a certain number of `times`.
+
+For every debugger step the reported state is compared with the expected state.
+To consider the states a match:
+
+* The `SourceLocation` must match in both states. Omitted fields in the
+`SourceLocation` dictionary are ignored; they always match.
+* Each `expr` in `watches` in the expected state can either be a dictionary
+with the fields shown above, or a string representing its value. In either
+case, the actual value of `expr` in the debugger must match.
+* The function name and inline status are not considered.
+
+### Heuristic
+[TODO]
+
+
+---
+## DexExpectStepKind
+    DexExpectStepKind(kind, times)
+
+    Args:
+      kind (str): Expected step kind.
+      times (int): Expected number of encounters.
+
+### Description
+Expect to see a particular step `kind` a number of `times` while stepping
+through the program.
+
+`kind` must be one of:
+
+`FUNC`: The first step into a function which is defined in the test
+directory.</br>
+`FUNC_EXTERNAL`: A step over a function which is not defined in the test
+directory.</br>
+`FUNC_UNKNOWN`: The first step over a function an unknown definition
+location.</br>
+`VERTICAL_FORWARD`: A step onto a line after the previous step line in this
+frame.</br>
+`VERTICAL_BACKWARD`: A step onto a line before the previous step line in
+this frame.</br>
+`HORIZONTAL_FORWARD`: A step forward on the same line as the previous step in
+this frame.</br>
+`HORIZONTAL_BACKWARD`: A step backward on the same line as the previous step
+in this frame.</br>
+`SAME`: A step onto the same line and column as the previous step in this
+frame.</br>
+
+### Heuristic
+[TODO]
+
+
+---
+## DexExpectStepOrder
+    DexExpectStepOrder(*order)
+
+    Arg list:
+      order (int): One or more indices.
+
+### Description
+Expect the line every `DexExpectStepOrder` is found on to be stepped on in
+`order`. Each instance must have a set of unique ascending indices.
+
+### Heuristic
+[TODO]
+
+
+---
+## DexExpectWatchType
+    DexExpectWatchType(expr, *types [,**from_line=1][,**to_line=Max]
+                        [,**on_line][,**require_in_order=True])
+
+    Args:
+        expr (str): expression to evaluate.
+
+    Arg list:
+        types (str): At least one expected type. NOTE: string type.
+
+    Keyword args:
+        from_line (int): Evaluate the expression from this line. Defaults to 1.
+        to_line (int): Evaluate the expression to this line. Defaults to end of
+            source.
+        on_line (int): Only evaluate the expression on this line. If provided,
+            this overrides from_line and to_line.
+        require_in_order (bool): If False the values can appear in any order.
+
+### Description
+Expect the expression `expr` to evaluate be evaluated and have each evaluation's
+type checked against the list of `types`
+
+### Heuristic
+[TODO]
+
+
+---
+## DexExpectWatchValue
+    DexExpectWatchValue(expr, *values [,**from_line=1][,**to_line=Max]
+                        [,**on_line][,**require_in_order=True])
+
+    Args:
+        expr (str): expression to evaluate.
+
+    Arg list:
+        values (str): At least one expected value. NOTE: string type.
+
+    Keyword args:
+        from_line (int): Evaluate the expression from this line. Defaults to 1.
+        to_line (int): Evaluate the expression to this line. Defaults to end of
+            source.
+        on_line (int): Only evaluate the expression on this line. If provided,
+            this overrides from_line and to_line.
+        require_in_order (bool): If False the values can appear in any order.
+
+### Description
+Expect the expression `expr` to evaluate to the list of `values`
+sequentially.
+
+### Heuristic
+[TODO]
+
+
+---
+## DexUnreachable
+    DexUnreachable()
+
+### Description
+Expect the source line this is found on will never be stepped on to.
+
+### Heuristic
+[TODO]
+
+
+----
+## DexLabel
+    DexLabel(name)
+
+    Args:
+        name (str): A unique name for this line.
+
+### Description
+Name the line this command is found on. Line names can be referenced by other
+commands expecting line number arguments.
+For example, `DexExpectWatchValues(..., on_line='my_line_name')`.
+
+### Heuristic
+This command does not contribute to the heuristic score.
+
+
+---
+## DexWatch
+    DexWatch(*expressions)
+
+    Arg list:
+        expressions (str): `expression` to evaluate on this line.
+
+### Description
+[Deprecated] Evaluate each given `expression` when the debugger steps onto the
+line this command is found on.
+
+### Heuristic
+[Deprecated]
diff --git a/debuginfo-tests/dexter/LICENSE.txt b/debuginfo-tests/dexter/LICENSE.txt
new file mode 100644 (file)
index 0000000..fa6ac54
--- /dev/null
@@ -0,0 +1,279 @@
+==============================================================================
+The LLVM Project is under the Apache License v2.0 with LLVM Exceptions:
+==============================================================================
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+    1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+    2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+    3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+    4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+    5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+    6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+    7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+    8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+    9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+    END OF TERMS AND CONDITIONS
+
+    APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+    Copyright [yyyy] [name of copyright owner]
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+
+---- LLVM Exceptions to the Apache 2.0 License ----
+
+As an exception, if, as a result of your compiling your source code, portions
+of this Software are embedded into an Object form of such source code, you
+may redistribute such embedded portions in such Object form without complying
+with the conditions of Sections 4(a), 4(b) and 4(d) of the License.
+
+In addition, if you combine or link compiled forms of this Software with
+software that is licensed under the GPLv2 ("Combined Software") and if a
+court of competent jurisdiction determines that the patent provision (Section
+3), the indemnity provision (Section 9) or other Section of the License
+conflicts with the conditions of the GPLv2, you may retroactively and
+prospectively choose to deem waived or otherwise exclude such Section(s) of
+the License, but only in their entirety and only with respect to the Combined
+Software.
+
+==============================================================================
+Software from third parties included in the LLVM Project:
+==============================================================================
+The LLVM Project contains third party software which is under different license
+terms. All such code will be identified clearly using at least one of two
+mechanisms:
+1) It will be in a separate directory tree with its own `LICENSE.txt` or
+   `LICENSE` file at the top containing the specific license and restrictions
+   which apply to that software, or
+2) It will contain specific license and restriction terms at the top of every
+   file.
+
+==============================================================================
+Legacy LLVM License (https://llvm.org/docs/DeveloperPolicy.html#legacy):
+==============================================================================
+University of Illinois/NCSA
+Open Source License
+
+Copyright (c) 2003-2019 University of Illinois at Urbana-Champaign.
+All rights reserved.
+
+Developed by:
+
+    LLVM Team
+
+    University of Illinois at Urbana-Champaign
+
+    http://llvm.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal with
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimers.
+
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimers in the
+      documentation and/or other materials provided with the distribution.
+
+    * Neither the names of the LLVM Team, University of Illinois at
+      Urbana-Champaign, nor the names of its contributors may be used to
+      endorse or promote products derived from this Software without specific
+      prior written permission.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE
+SOFTWARE.
+
diff --git a/debuginfo-tests/dexter/README.md b/debuginfo-tests/dexter/README.md
new file mode 100644 (file)
index 0000000..1da6fa1
--- /dev/null
@@ -0,0 +1,304 @@
+# DExTer (Debugging Experience Tester)
+
+## Introduction
+
+DExTer is a suite of tools used to evaluate the "User Debugging Experience". DExTer drives an external debugger, running on small test programs, and collects information on the behavior at each debugger step to provide quantitative values that indicate the quality of the debugging experience.
+
+## Supported Debuggers
+
+DExTer currently supports the Visual Studio 2015 and Visual Studio 2017 debuggers via the [DTE interface](https://docs.microsoft.com/en-us/dotnet/api/envdte.dte), and LLDB via its [Python interface](https://lldb.llvm.org/python-reference.html). GDB is not currently supported.
+
+The following command evaluates your environment, listing the available and compatible debuggers:
+
+    dexter.py list-debuggers
+
+## Dependencies
+[TODO] Add a requirements.txt or an install.py and document it here.
+
+### Python 3.6
+
+DExTer requires python version 3.6 or greater.
+
+### pywin32 python package
+
+This is required to access the DTE interface for the Visual Studio debuggers.
+
+    <python-executable> -m pip install pywin32
+
+### clang
+
+DExTer is current compatible with 'clang' and 'clang-cl' compiler drivers.  The compiler must be available for DExTer, for example the following command should successfully build a runnable executable.
+
+     <compiler-executable> tests/nostdlib/fibonacci/test.cpp
+
+## Running a test case
+
+The following DExTer commands build the test.cpp from the tests/nostdlib/fibonacci directory and quietly runs it on the Visual Studio debugger, reporting the debug experience heuristic.  The first command builds with no optimizations (/Od) and scores 1.0000.  The second command builds with optimizations (/Ox) and scores 0.2832 which suggests a worse debugging experience.
+
+    dexter.py test --builder clang-cl_vs2015 --debugger vs2017 --cflags="/Od /Zi" --ldflags="/Zi" -- tests/nostdlib/fibonacci
+    fibonacci = (1.0000)
+
+    dexter.py test --builder clang-cl_vs2015 --debugger vs2017 --cflags="/Ox /Zi" --ldflags="/Zi" -- tests/nostdlib/fibonacci
+    fibonacci = (0.2832)
+
+## An example test case
+
+The sample test case (tests/nostdlib/fibonacci) looks like this:
+
+    1.  #ifdef _MSC_VER
+    2.  # define DEX_NOINLINE __declspec(noinline)
+    3.  #else
+    4.  # define DEX_NOINLINE __attribute__((__noinline__))
+    5.  #endif
+    6.
+    7.  DEX_NOINLINE
+    8.  void Fibonacci(int terms, int& total)
+    9.  {
+    0.      int first = 0;
+    11.     int second = 1;
+    12.     for (int i = 0; i < terms; ++i)
+    13.     {
+    14.         int next = first + second; // DexLabel('start')
+    15.         total += first;
+    16.         first = second;
+    17.         second = next;             // DexLabel('end')
+    18.     }
+    19. }
+    20.
+    21. int main()
+    22. {
+    23.     int total = 0;
+    24.     Fibonacci(5, total);
+    25.     return total;
+    26. }
+    27.
+    28. /*
+    29. DexExpectWatchValue('i', '0', '1', '2', '3', '4',
+    30.                     from_line='start', to_line='end')
+    31. DexExpectWatchValue('first', '0', '1', '2', '3', '5',
+    32.                     from_line='start', to_line='end')
+    33. DexExpectWatchValue('second', '1', '2', '3', '5',
+    34                      from_line='start', to_line='end')
+    35. DexExpectWatchValue('total', '0', '1', '2', '4', '7',
+    36.                     from_line='start', to_line='end')
+    37. DexExpectWatchValue('next', '1', '2', '3', '5', '8',
+    38.                     from_line='start', to_line='end')
+    39. DexExpectWatchValue('total', '7', on_line=25)
+    40. DexExpectStepKind('FUNC_EXTERNAL', 0)
+    41. */
+
+[DexLabel][1] is used to give a name to a line number.
+
+The [DexExpectWatchValue][2] command states that an expression, e.g. `i`, should
+have particular values, `'0', '1', '2', '3','4'`, sequentially over the program
+lifetime on particular lines. You can refer to a named line or simply the line
+number (See line 39).
+
+At the end of the test is the following line:
+
+    DexExpectStepKind('FUNC_EXTERNAL', 0)
+
+This [DexExpectStepKind][3] command indicates that we do not expect the debugger
+to step into a file outside of the test directory.
+
+[1]: Commands.md#DexLabel
+[2]: Commands.md#DexExpectWatchValue
+[3]: Commands.md#DexExpectStepKind
+
+## Detailed DExTer reports
+
+Running the command below launches the tests/nostdlib/fibonacci test case in DExTer, using clang-cl as the compiler, Visual Studio 2017 as the debugger, and producing a detailed report:
+
+    $ dexter.py test --builder clang-cl_vs2015 --debugger vs2017 --cflags="/Ox /Zi" --ldflags="/Zi" -v -- tests/nostdlib/fibonacci
+
+The detailed report is enabled by `-v` and shows a breakdown of the information from each debugger step. For example:
+
+    fibonacci = (0.2832)
+
+    ## BEGIN ##
+    [1, "main", "c:\\dexter\\tests\\nostdlib\\fibonacci\\test.cpp", 23, 1, "BREAKPOINT", "FUNC", {}]
+    [2, "main", "c:\\dexter\\tests\\nostdlib\\fibonacci\\test.cpp", 24, 1, "BREAKPOINT", "VERTICAL_FORWARD", {}]
+    [3, "main", "c:\\dexter\\tests\\nostdlib\\fibonacci\\test.cpp", 25, 1, "BREAKPOINT", "VERTICAL_FORWARD", {}]
+    .   [4, "Fibonacci", "c:\\dexter\\tests\\nostdlib\\fibonacci\\test.cpp", 13, 1, "BREAKPOINT", "FUNC", {}]
+    .   [5, "Fibonacci", "c:\\dexter\\tests\\nostdlib\\fibonacci\\test.cpp", 16, 1, "BREAKPOINT", "VERTICAL_FORWARD", {"i": "Variable is optimized away and not available.", "next": "Variable is optimized away and not available.", "second": "Variable is optimized away and not available.", "total": "0", "first": "Variable is optimized away and not available."}]
+    .   [6, "Fibonacci", "c:\\dexter\\tests\\nostdlib\\fibonacci\\test.cpp", 13, 1, "BREAKPOINT", "VERTICAL_BACKWARD", {}]
+    .   [7, "Fibonacci", "c:\\dexter\\tests\\nostdlib\\fibonacci\\test.cpp", 15, 1, "BREAKPOINT", "VERTICAL_FORWARD", {"i": "Variable is optimized away and not available.", "second": "Variable is optimized away and not available.", "total": "0", "first": "Variable is optimized away and not available."}]
+    .   [8, "Fibonacci", "c:\\dexter\\tests\\nostdlib\\fibonacci\\test.cpp", 16, 1, "BREAKPOINT", "VERTICAL_FORWARD", {"i": "Variable is optimized away and not available.", "next": "Variable is optimized away and not available.", "second": "Variable is optimized away and not available.", "total": "0", "first": "Variable is optimized away and not available."}]
+    .   [9, "Fibonacci", "c:\\dexter\\tests\\nostdlib\\fibonacci\\test.cpp", 15, 1, "BREAKPOINT", "VERTICAL_BACKWARD", {"i": "Variable is optimized away and not available.", "second": "1", "total": "0", "first": "0"}]
+    .   [10, "Fibonacci", "c:\\dexter\\tests\\nostdlib\\fibonacci\\test.cpp", 13, 1, "BREAKPOINT", "VERTICAL_BACKWARD", {}]
+    .   [11, "Fibonacci", "c:\\dexter\\tests\\nostdlib\\fibonacci\\test.cpp", 16, 1, "BREAKPOINT", "VERTICAL_FORWARD", {"i": "Variable is optimized away and not available.", "next": "Variable is optimized away and not available.", "second": "Variable is optimized away and not available.", "total": "0", "first": "Variable is optimized away and not available."}]
+    .   [12, "Fibonacci", "c:\\dexter\\tests\\nostdlib\\fibonacci\\test.cpp", 15, 1, "BREAKPOINT", "VERTICAL_BACKWARD", {"i": "Variable is optimized away and not available.", "second": "1", "total": "0", "first": "1"}]
+    .   [13, "Fibonacci", "c:\\dexter\\tests\\nostdlib\\fibonacci\\test.cpp", 13, 1, "BREAKPOINT", "VERTICAL_BACKWARD", {}]
+    .   [14, "Fibonacci", "c:\\dexter\\tests\\nostdlib\\fibonacci\\test.cpp", 16, 1, "BREAKPOINT", "VERTICAL_FORWARD", {"i": "Variable is optimized away and not available.", "next": "Variable is optimized away and not available.", "second": "Variable is optimized away and not available.", "total": "0", "first": "Variable is optimized away and not available."}]
+    .   [15, "Fibonacci", "c:\\dexter\\tests\\nostdlib\\fibonacci\\test.cpp", 15, 1, "BREAKPOINT", "VERTICAL_BACKWARD", {"i": "Variable is optimized away and not available.", "second": "2", "total": "0", "first": "1"}]
+    .   [16, "Fibonacci", "c:\\dexter\\tests\\nostdlib\\fibonacci\\test.cpp", 13, 1, "BREAKPOINT", "VERTICAL_BACKWARD", {}]
+    .   [17, "Fibonacci", "c:\\dexter\\tests\\nostdlib\\fibonacci\\test.cpp", 16, 1, "BREAKPOINT", "VERTICAL_FORWARD", {"i": "Variable is optimized away and not available.", "next": "Variable is optimized away and not available.", "second": "Variable is optimized away and not available.", "total": "0", "first": "Variable is optimized away and not available."}]
+    .   [18, "Fibonacci", "c:\\dexter\\tests\\nostdlib\\fibonacci\\test.cpp", 15, 1, "BREAKPOINT", "VERTICAL_BACKWARD", {"i": "Variable is optimized away and not available.", "second": "3", "total": "0", "first": "2"}]
+    .   [19, "Fibonacci", "c:\\dexter\\tests\\nostdlib\\fibonacci\\test.cpp", 13, 1, "BREAKPOINT", "VERTICAL_BACKWARD", {}]
+    .   [20, "Fibonacci", "c:\\dexter\\tests\\nostdlib\\fibonacci\\test.cpp", 16, 1, "BREAKPOINT", "VERTICAL_FORWARD", {"i": "Variable is optimized away and not available.", "next": "Variable is optimized away and not available.", "second": "Variable is optimized away and not available.", "total": "0", "first": "Variable is optimized away and not available."}]
+    .   [21, "Fibonacci", "c:\\dexter\\tests\\nostdlib\\fibonacci\\test.cpp", 15, 1, "BREAKPOINT", "VERTICAL_BACKWARD", {"i": "Variable is optimized away and not available.", "second": "5", "total": "0", "first": "3"}]
+    .   [22, "Fibonacci", "c:\\dexter\\tests\\nostdlib\\fibonacci\\test.cpp", 13, 1, "BREAKPOINT", "VERTICAL_BACKWARD", {}]
+    .   [23, "Fibonacci", "c:\\dexter\\tests\\nostdlib\\fibonacci\\test.cpp", 16, 1, "BREAKPOINT", "VERTICAL_FORWARD", {"i": "Variable is optimized away and not available.", "next": "Variable is optimized away and not available.", "second": "Variable is optimized away and not available.", "total": "0", "first": "Variable is optimized away and not available."}]
+    .   [24, "Fibonacci", "c:\\dexter\\tests\\nostdlib\\fibonacci\\test.cpp", 20, 1, "BREAKPOINT", "VERTICAL_FORWARD", {}]
+    [25, "main", "c:\\dexter\\tests\\nostdlib\\fibonacci\\test.cpp", 26, 1, "BREAKPOINT", "FUNC", {"total": "7"}]
+    ## END (25 steps) ##
+
+
+    step kind differences [0/1]
+        FUNC_EXTERNAL:
+        0
+
+    test.cpp:15-18 [first] [9/21]
+        expected encountered values:
+        0
+        1
+        2
+        3
+
+        missing values:
+        5 [-6]
+
+        result optimized away:
+        step 5 (Variable is optimized away and not available.) [-3]
+        step 7 (Variable is optimized away and not available.)
+        step 8 (Variable is optimized away and not available.)
+        step 11 (Variable is optimized away and not available.)
+        step 14 (Variable is optimized away and not available.)
+        step 17 (Variable is optimized away and not available.)
+        step 20 (Variable is optimized away and not available.)
+        step 23 (Variable is optimized away and not available.)
+
+    test.cpp:15-18 [i] [15/21]
+        result optimized away:
+        step 5 (Variable is optimized away and not available.) [-3]
+        step 7 (Variable is optimized away and not available.) [-3]
+        step 8 (Variable is optimized away and not available.) [-3]
+        step 9 (Variable is optimized away and not available.) [-3]
+        step 11 (Variable is optimized away and not available.) [-3]
+        step 12 (Variable is optimized away and not available.)
+        step 14 (Variable is optimized away and not available.)
+        step 15 (Variable is optimized away and not available.)
+        step 17 (Variable is optimized away and not available.)
+        step 18 (Variable is optimized away and not available.)
+        step 20 (Variable is optimized away and not available.)
+        step 21 (Variable is optimized away and not available.)
+        step 23 (Variable is optimized away and not available.)
+
+    test.cpp:15-18 [second] [21/21]
+        expected encountered values:
+        1
+        2
+        3
+        5
+
+        result optimized away:
+        step 5 (Variable is optimized away and not available.) [-3]
+        step 7 (Variable is optimized away and not available.) [-3]
+        step 8 (Variable is optimized away and not available.) [-3]
+        step 11 (Variable is optimized away and not available.) [-3]
+        step 14 (Variable is optimized away and not available.) [-3]
+        step 17 (Variable is optimized away and not available.) [-3]
+        step 20 (Variable is optimized away and not available.) [-3]
+        step 23 (Variable is optimized away and not available.)
+
+    test.cpp:15-18 [total] [21/21]
+        expected encountered values:
+        0
+
+        missing values:
+        1 [-6]
+        2 [-6]
+        4 [-6]
+        7 [-3]
+
+    test.cpp:16-18 [next] [15/21]
+        result optimized away:
+        step 5 (Variable is optimized away and not available.) [-3]
+        step 8 (Variable is optimized away and not available.) [-3]
+        step 11 (Variable is optimized away and not available.) [-3]
+        step 14 (Variable is optimized away and not available.) [-3]
+        step 17 (Variable is optimized away and not available.) [-3]
+        step 20 (Variable is optimized away and not available.)
+        step 23 (Variable is optimized away and not available.)
+
+    test.cpp:26 [total] [0/7]
+        expected encountered values:
+        7
+
+The first line
+
+    fibonacci =  (0.2832)
+
+shows a score of 0.2832 suggesting that unexpected behavior has been seen.  This score is on scale of 0.0000 to 1.000, with 0.000 being the worst score possible and 1.000 being the best score possible.  The verbose output shows the reason for any scoring.  For example:
+
+    test.cpp:15-18 [first] [9/21]
+        expected encountered values:
+        0
+        1
+        2
+        3
+
+        missing values:
+        5 [-6]
+
+        result optimized away:
+        step 5 (Variable is optimized away and not available.) [-3]
+        step 7 (Variable is optimized away and not available.)
+        step 8 (Variable is optimized away and not available.)
+        step 11 (Variable is optimized away and not available.)
+        step 14 (Variable is optimized away and not available.)
+        step 17 (Variable is optimized away and not available.)
+        step 20 (Variable is optimized away and not available.)
+        step 23 (Variable is optimized away and not available.)
+
+shows that for `first` the expected values 0, 1, 2 and 3 were seen, 5 was not.  On some steps the variable was reported as being optimized away.
+
+## Writing new test cases
+
+Each test requires a `test.cfg` file.  Currently the contents of this file are not read, but its presence is used to determine the root directory of a test. In the future, configuration variables for the test such as supported language modes may be stored in this file. Use the various [commands](Commands.md) to encode debugging expectations.
+
+## Additional tools
+
+For clang-based compilers, the `clang-opt-bisect` tool can be used to get a breakdown of which LLVM passes may be contributing to debugging experience issues.  For example:
+
+    $ dexter.py clang-opt-bisect tests/nostdlib/fibonacci --builder clang-cl --debugger vs2017 --cflags="/Ox /Zi" --ldflags="/Zi"
+
+    pass 1/211 =  (1.0000)  (0.0000) [Simplify the CFG on function (?Fibonacci@@YAXHAEAH@Z)]
+    pass 2/211 =  (0.7611) (-0.2389) [SROA on function (?Fibonacci@@YAXHAEAH@Z)]
+    pass 3/211 =  (0.7611)  (0.0000) [Early CSE on function (?Fibonacci@@YAXHAEAH@Z)]
+    pass 4/211 =  (0.7611)  (0.0000) [Simplify the CFG on function (main)]
+    pass 5/211 =  (0.7611)  (0.0000) [SROA on function (main)]
+    pass 6/211 =  (0.7611)  (0.0000) [Early CSE on function (main)]
+    pass 7/211 =  (0.7611)  (0.0000) [Infer set function attributes on module (c:\dexter\tests\fibonacci\test.cpp)]
+    pass 8/211 =  (0.7611)  (0.0000) [Interprocedural Sparse Conditional Constant Propagation on module (c:\dexter\tests\fibonacci\test.cpp)]
+    pass 9/211 =  (0.7611)  (0.0000) [Called Value Propagation on module (c:\dexter\tests\fibonacci\test.cpp)]
+    pass 10/211 =  (0.7611)  (0.0000) [Global Variable Optimizer on module (c:\dexter\tests\fibonacci\test.cpp)]
+    pass 11/211 =  (0.7611)  (0.0000) [Promote Memory to Register on function (?Fibonacci@@YAXHAEAH@Z)]
+    pass 12/211 =  (0.7611)  (0.0000) [Promote Memory to Register on function (main)]
+    pass 13/211 =  (0.7611)  (0.0000) [Dead Argument Elimination on module (c:\dexter\tests\fibonacci\test.cpp)]
+    pass 14/211 =  (0.7611)  (0.0000) [Combine redundant instructions on function (?Fibonacci@@YAXHAEAH@Z)]
+    pass 15/211 =  (0.7611)  (0.0000) [Simplify the CFG on function (?Fibonacci@@YAXHAEAH@Z)]a
+    pass 16/211 =  (0.7345) (-0.0265) [Combine redundant instructions on function (main)]
+    pass 17/211 =  (0.7345)  (0.0000) [Simplify the CFG on function (main)]
+    pass 18/211 =  (0.7345)  (0.0000) [Remove unused exception handling info on SCC (?Fibonacci@@YAXHAEAH@Z)]
+    pass 19/211 =  (0.7345)  (0.0000) [Function Integration/Inlining on SCC (?Fibonacci@@YAXHAEAH@Z)]
+    pass 20/211 =  (0.7345)  (0.0000) [Deduce function attributes on SCC (?Fibonacci@@YAXHAEAH@Z)]
+    pass 21/211 =  (0.7345)  (0.0000) [SROA on function (?Fibonacci@@YAXHAEAH@Z)]
+    pass 22/211 =  (0.7345)  (0.0000) [Early CSE w/ MemorySSA on function (?Fibonacci@@YAXHAEAH@Z)]
+    pass 23/211 =  (0.7345)  (0.0000) [Speculatively execute instructions if target has divergent branches on function (?Fibonacci@@YAXHAEAH@Z)]
+    pass 24/211 =  (0.7345)  (0.0000) [Jump Threading on function (?Fibonacci@@YAXHAEAH@Z)]
+    pass 25/211 =  (0.7345)  (0.0000) [Value Propagation on function (?Fibonacci@@YAXHAEAH@Z)]
+    pass 26/211 =  (0.7345)  (0.0000) [Simplify the CFG on function (?Fibonacci@@YAXHAEAH@Z)]
+    pass 27/211 =  (0.7345)  (0.0000) [Combine redundant instructions on function (?Fibonacci@@YAXHAEAH@Z)]
+    pass 28/211 =  (0.7345)  (0.0000) [Tail Call Elimination on function (?Fibonacci@@YAXHAEAH@Z)]
+    pass 29/211 =  (0.7345)  (0.0000) [Simplify the CFG on function (?Fibonacci@@YAXHAEAH@Z)]
+    pass 30/211 =  (0.7345)  (0.0000) [Reassociate expressions on function (?Fibonacci@@YAXHAEAH@Z)]
+    pass 31/211 =  (0.8673)  (0.1327) [Rotate Loops on loop]
+    pass 32/211 =  (0.5575) (-0.3097) [Loop Invariant Code Motion on loop]
+    pass 33/211 =  (0.5575)  (0.0000) [Unswitch loops on loop]
+    pass 34/211 =  (0.5575)  (0.0000) [Simplify the CFG on function (?Fibonacci@@YAXHAEAH@Z)]
+    pass 35/211 =  (0.5575)  (0.0000) [Combine redundant instructions on function (?Fibonacci@@YAXHAEAH@Z)]
+    pass 36/211 =  (0.5575)  (0.0000) [Induction Variable Simplification on loop]
+    pass 37/211 =  (0.5575)  (0.0000) [Recognize loop idioms on loop]
+    <output-snipped>
+
diff --git a/debuginfo-tests/dexter/dex/__init__.py b/debuginfo-tests/dexter/dex/__init__.py
new file mode 100644 (file)
index 0000000..d2a290b
--- /dev/null
@@ -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 (file)
index 0000000..a2bab86
--- /dev/null
@@ -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 (file)
index 0000000..b6677aa
--- /dev/null
@@ -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 (file)
index 0000000..3bf0ca4
--- /dev/null
@@ -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 (executable)
index 0000000..f69f51c
--- /dev/null
@@ -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 (executable)
index 0000000..9cf4cdd
--- /dev/null
@@ -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 (file)
index 0000000..ea0d441
--- /dev/null
@@ -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 (file)
index 0000000..a83e4d4
--- /dev/null
@@ -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 (file)
index 0000000..49e9086
--- /dev/null
@@ -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 (file)
index 0000000..3b9a2d5
--- /dev/null
@@ -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 (file)
index 0000000..afcb9c5
--- /dev/null
@@ -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 (file)
index 0000000..70da546
--- /dev/null
@@ -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 (file)
index 0000000..7833583
--- /dev/null
@@ -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 (file)
index 0000000..6370f5d
--- /dev/null
@@ -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 (file)
index 0000000..4342bc5
--- /dev/null
@@ -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 (file)
index 0000000..e6422d1
--- /dev/null
@@ -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 (file)
index 0000000..f2336de
--- /dev/null
@@ -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 (file)
index 0000000..d6da006
--- /dev/null
@@ -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 (file)
index 0000000..8a0325e
--- /dev/null
@@ -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 (file)
index 0000000..188a5d8
--- /dev/null
@@ -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 (file)
index 0000000..2dfa3a3
--- /dev/null
@@ -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 (file)
index 0000000..8013ceb
--- /dev/null
@@ -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 (file)
index 0000000..2f246a3
--- /dev/null
@@ -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 (file)
index 0000000..3c4fdec
--- /dev/null
@@ -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 (file)
index 0000000..f9b8642
--- /dev/null
@@ -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 (file)
index 0000000..3c458f9
--- /dev/null
@@ -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 (file)
index 0000000..c966d8c
--- /dev/null
@@ -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 (file)
index 0000000..a65e4de
--- /dev/null
@@ -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 (file)
index 0000000..38585c8
--- /dev/null
@@ -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 (file)
index 0000000..66d01f0
--- /dev/null
@@ -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 (file)
index 0000000..8bd7f60
--- /dev/null
@@ -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 (file)
index 0000000..30a62f6
--- /dev/null
@@ -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 (file)
index 0000000..bc998fa
--- /dev/null
@@ -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 (file)
index 0000000..2775af3
--- /dev/null
@@ -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 (file)
index 0000000..0e9844a
--- /dev/null
@@ -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 (file)
index 0000000..0c9197a
--- /dev/null
@@ -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 (file)
index 0000000..425d3c2
--- /dev/null
@@ -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 (file)
index 0000000..1282f2d
--- /dev/null
@@ -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 (file)
index 0000000..596dc31
--- /dev/null
@@ -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 (file)
index 0000000..af6edcd
--- /dev/null
@@ -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 (file)
index 0000000..f2f7575
--- /dev/null
@@ -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 (file)
index 0000000..35fefac
--- /dev/null
@@ -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 (file)
index 0000000..0bce5b5
--- /dev/null
@@ -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 (file)
index 0000000..1194aff
--- /dev/null
@@ -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 (file)
index 0000000..b94a1fb
--- /dev/null
@@ -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 (file)
index 0000000..5956db6
--- /dev/null
@@ -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 (file)
index 0000000..7638e8b
--- /dev/null
@@ -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 (file)
index 0000000..a2c0523
--- /dev/null
@@ -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 (file)
index 0000000..52a56a8
--- /dev/null
@@ -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 (file)
index 0000000..4f05189
--- /dev/null
@@ -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 (file)
index 0000000..8111968
--- /dev/null
@@ -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 (file)
index 0000000..9d532ac
--- /dev/null
@@ -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 (file)
index 0000000..463a2c1
--- /dev/null
@@ -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 (file)
index 0000000..205b767
--- /dev/null
@@ -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 (file)
index 0000000..2a143f6
--- /dev/null
@@ -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 (file)
index 0000000..78fb4f7
--- /dev/null
@@ -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 (file)
index 0000000..7e00fc5
--- /dev/null
@@ -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 (file)
index 0000000..eb6ba94
--- /dev/null
@@ -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 (file)
index 0000000..76d1261
--- /dev/null
@@ -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 (file)
index 0000000..a2fc296
--- /dev/null
@@ -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,
+                       &nb