summaryrefslogtreecommitdiffstats
path: root/testing/mozbase/mozdebug
diff options
context:
space:
mode:
Diffstat (limited to 'testing/mozbase/mozdebug')
-rw-r--r--testing/mozbase/mozdebug/mozdebug/__init__.py32
-rwxr-xr-xtesting/mozbase/mozdebug/mozdebug/mozdebug.py316
-rw-r--r--testing/mozbase/mozdebug/setup.cfg2
-rw-r--r--testing/mozbase/mozdebug/setup.py33
-rwxr-xr-xtesting/mozbase/mozdebug/tests/fake_debuggers/cgdb/cgdb0
-rwxr-xr-xtesting/mozbase/mozdebug/tests/fake_debuggers/devenv/devenv.exe0
-rwxr-xr-xtesting/mozbase/mozdebug/tests/fake_debuggers/gdb/gdb0
-rwxr-xr-xtesting/mozbase/mozdebug/tests/fake_debuggers/lldb/lldb0
-rwxr-xr-xtesting/mozbase/mozdebug/tests/fake_debuggers/wdexpress/wdexpress.exe0
-rw-r--r--testing/mozbase/mozdebug/tests/manifest.ini3
-rw-r--r--testing/mozbase/mozdebug/tests/test.py67
11 files changed, 453 insertions, 0 deletions
diff --git a/testing/mozbase/mozdebug/mozdebug/__init__.py b/testing/mozbase/mozdebug/mozdebug/__init__.py
new file mode 100644
index 0000000000..add9214a6f
--- /dev/null
+++ b/testing/mozbase/mozdebug/mozdebug/__init__.py
@@ -0,0 +1,32 @@
+# flake8: noqa
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+This module contains a set of function to gather information about the
+debugging capabilities of the platform. It allows to look for a specific
+debugger or to query the system for a compatible/default debugger.
+
+The following simple example looks for the default debugger on the
+current platform and launches a debugger process with the correct
+debugger-specific arguments:
+
+::
+
+ import mozdebug
+
+ debugger = mozdebug.get_default_debugger_name()
+ debuggerInfo = mozdebug.get_debugger_info(debugger)
+
+ debuggeePath = "toDebug"
+
+ processArgs = [self.debuggerInfo.path] + self.debuggerInfo.args
+ processArgs.append(debuggeePath)
+
+ run_process(args, ...)
+
+"""
+from __future__ import absolute_import
+
+from .mozdebug import *
diff --git a/testing/mozbase/mozdebug/mozdebug/mozdebug.py b/testing/mozbase/mozdebug/mozdebug/mozdebug.py
new file mode 100755
index 0000000000..3905fdd331
--- /dev/null
+++ b/testing/mozbase/mozdebug/mozdebug/mozdebug.py
@@ -0,0 +1,316 @@
+#!/usr/bin/env python
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from __future__ import absolute_import, print_function
+
+import json
+import os
+import mozinfo
+import sys
+from collections import namedtuple
+from distutils.spawn import find_executable
+from subprocess import check_output
+
+__all__ = [
+ "get_debugger_info",
+ "get_default_debugger_name",
+ "DebuggerSearch",
+ "get_default_valgrind_args",
+ "DebuggerInfo",
+]
+
+"""
+Map of debugging programs to information about them, like default arguments
+and whether or not they are interactive.
+
+To add support for a new debugger, simply add the relative entry in
+_DEBUGGER_INFO and optionally update the _DEBUGGER_PRIORITIES.
+"""
+_DEBUGGER_INFO = {
+ # gdb requires that you supply the '--args' flag in order to pass arguments
+ # after the executable name to the executable.
+ "gdb": {"interactive": True, "args": ["-q", "--args"]},
+ "cgdb": {"interactive": True, "args": ["-q", "--args"]},
+ "rust-gdb": {"interactive": True, "args": ["-q", "--args"]},
+ "lldb": {"interactive": True, "args": ["--"], "requiresEscapedArgs": True},
+ # Visual Studio Debugger Support.
+ "devenv.exe": {"interactive": True, "args": ["-debugexe"]},
+ # Visual C++ Express Debugger Support.
+ "wdexpress.exe": {"interactive": True, "args": ["-debugexe"]},
+ # Windows Development Kit super-debugger.
+ "windbg.exe": {
+ "interactive": True,
+ },
+}
+
+# Maps each OS platform to the preferred debugger programs found in _DEBUGGER_INFO.
+_DEBUGGER_PRIORITIES = {
+ "win": ["devenv.exe", "wdexpress.exe"],
+ "linux": ["gdb", "cgdb", "lldb"],
+ "mac": ["lldb", "gdb"],
+ "android": ["gdb"],
+ "unknown": ["gdb"],
+}
+
+
+DebuggerInfo = namedtuple(
+ "DebuggerInfo", ["path", "interactive", "args", "requiresEscapedArgs"]
+)
+
+
+def _windbg_installation_paths():
+ programFilesSuffixes = ["", " (x86)"]
+ programFiles = "C:/Program Files"
+ # Try the most recent versions first.
+ windowsKitsVersions = ["10", "8.1", "8"]
+
+ for suffix in programFilesSuffixes:
+ windowsKitsPrefix = os.path.join(programFiles + suffix, "Windows Kits")
+ for version in windowsKitsVersions:
+ yield os.path.join(
+ windowsKitsPrefix, version, "Debuggers", "x64", "windbg.exe"
+ )
+
+
+def _vswhere_path():
+ try:
+ import buildconfig
+
+ path = os.path.join(buildconfig.topsrcdir, "build", "win32", "vswhere.exe")
+ if os.path.isfile(path):
+ return path
+ except ImportError:
+ pass
+ # Hope it's available on PATH!
+ return "vswhere.exe"
+
+
+def get_debugger_path(debugger):
+ """
+ Get the full path of the debugger.
+
+ :param debugger: The name of the debugger.
+ """
+
+ if mozinfo.os == "mac" and debugger == "lldb":
+ # On newer OSX versions System Integrity Protections prevents us from
+ # setting certain env vars for a process such as DYLD_LIBRARY_PATH if
+ # it's in a protected directory such as /usr/bin. This is the case for
+ # lldb, so we try to find an instance under the Xcode install instead.
+
+ # Attempt to use the xcrun util to find the path.
+ try:
+ path = check_output(
+ ["xcrun", "--find", "lldb"], universal_newlines=True
+ ).strip()
+ if path:
+ return path
+ except Exception:
+ # Just default to find_executable instead.
+ pass
+
+ if mozinfo.os == "win" and debugger == "devenv.exe":
+ # Attempt to use vswhere to find the path.
+ try:
+ encoding = "mbcs" if sys.platform == "win32" else "utf-8"
+ vswhere = _vswhere_path()
+ vsinfo = check_output([vswhere, "-format", "json", "-latest"])
+ vsinfo = json.loads(vsinfo.decode(encoding, "replace"))
+ return os.path.join(
+ vsinfo[0]["installationPath"], "Common7", "IDE", "devenv.exe"
+ )
+ except Exception:
+ # Just default to find_executable instead.
+ pass
+
+ return find_executable(debugger)
+
+
+def get_debugger_info(debugger, debuggerArgs=None, debuggerInteractive=False):
+ """
+ Get the information about the requested debugger.
+
+ Returns a dictionary containing the |path| of the debugger executable,
+ if it will run in |interactive| mode, its arguments and whether it needs
+ to escape arguments it passes to the debugged program (|requiresEscapedArgs|).
+ If the debugger cannot be found in the system, returns |None|.
+
+ :param debugger: The name of the debugger.
+ :param debuggerArgs: If specified, it's the arguments to pass to the debugger,
+ as a string. Any debugger-specific separator arguments are appended after these
+ arguments.
+ :param debuggerInteractive: If specified, forces the debugger to be interactive.
+ """
+
+ debuggerPath = None
+
+ if debugger:
+ # Append '.exe' to the debugger on Windows if it's not present,
+ # so things like '--debugger=devenv' work.
+ if os.name == "nt" and not debugger.lower().endswith(".exe"):
+ debugger += ".exe"
+
+ debuggerPath = get_debugger_path(debugger)
+
+ if not debuggerPath:
+ # windbg is not installed with the standard set of tools, and it's
+ # entirely possible that the user hasn't added the install location to
+ # PATH, so we have to be a little more clever than normal to locate it.
+ # Just try to look for it in the standard installed location(s).
+ if debugger == "windbg.exe":
+ for candidate in _windbg_installation_paths():
+ if os.path.exists(candidate):
+ debuggerPath = candidate
+ break
+ else:
+ if os.path.exists(debugger):
+ debuggerPath = debugger
+
+ if not debuggerPath:
+ print("Error: Could not find debugger %s." % debugger)
+ print("Is it installed? Is it in your PATH?")
+ return None
+
+ debuggerName = os.path.basename(debuggerPath).lower()
+
+ def get_debugger_info(type, default):
+ if debuggerName in _DEBUGGER_INFO and type in _DEBUGGER_INFO[debuggerName]:
+ return _DEBUGGER_INFO[debuggerName][type]
+ return default
+
+ # Define a namedtuple to access the debugger information from the outside world.
+ debugger_arguments = []
+
+ if debuggerArgs:
+ # Append the provided debugger arguments at the end of the arguments list.
+ debugger_arguments += debuggerArgs.split()
+
+ debugger_arguments += get_debugger_info("args", [])
+
+ # Override the default debugger interactive mode if needed.
+ debugger_interactive = get_debugger_info("interactive", False)
+ if debuggerInteractive:
+ debugger_interactive = debuggerInteractive
+
+ d = DebuggerInfo(
+ debuggerPath,
+ debugger_interactive,
+ debugger_arguments,
+ get_debugger_info("requiresEscapedArgs", False),
+ )
+
+ return d
+
+
+# Defines the search policies to use in get_default_debugger_name.
+
+
+class DebuggerSearch:
+ OnlyFirst = 1
+ KeepLooking = 2
+
+
+def get_default_debugger_name(search=DebuggerSearch.OnlyFirst):
+ """
+ Get the debugger name for the default debugger on current platform.
+
+ :param search: If specified, stops looking for the debugger if the
+ default one is not found (|DebuggerSearch.OnlyFirst|) or keeps
+ looking for other compatible debuggers (|DebuggerSearch.KeepLooking|).
+ """
+
+ mozinfo.find_and_update_from_json()
+ os = mozinfo.info["os"]
+
+ # Find out which debuggers are preferred for use on this platform.
+ debuggerPriorities = _DEBUGGER_PRIORITIES[
+ os if os in _DEBUGGER_PRIORITIES else "unknown"
+ ]
+
+ # Finally get the debugger information.
+ for debuggerName in debuggerPriorities:
+ debuggerPath = get_debugger_path(debuggerName)
+ if debuggerPath:
+ return debuggerName
+ elif not search == DebuggerSearch.KeepLooking:
+ return None
+
+ return None
+
+
+# Defines default values for Valgrind flags.
+#
+# --smc-check=all-non-file is required to deal with code generation and
+# patching by the various JITS. Note that this is only necessary on
+# x86 and x86_64, but not on ARM. This flag is only necessary for
+# Valgrind versions prior to 3.11.
+#
+# --vex-iropt-register-updates=allregs-at-mem-access is required so that
+# Valgrind generates correct register values whenever there is a
+# segfault that is caught and handled. In particular OdinMonkey
+# requires this. More recent Valgrinds (3.11 and later) provide
+# --px-default=allregs-at-mem-access and
+# --px-file-backed=unwindregs-at-mem-access
+# which provide a significantly cheaper alternative, by restricting the
+# precise exception behaviour to JIT generated code only.
+#
+# --trace-children=yes is required to get Valgrind to follow into
+# content and other child processes. The resulting output can be
+# difficult to make sense of, and --child-silent-after-fork=yes
+# helps by causing Valgrind to be silent for the child in the period
+# after fork() but before its subsequent exec().
+#
+# --trace-children-skip lists processes that we are not interested
+# in tracing into.
+#
+# --leak-check=full requests full stack traces for all leaked blocks
+# detected at process exit.
+#
+# --show-possibly-lost=no requests blocks for which only an interior
+# pointer was found to be considered not leaked.
+#
+#
+# TODO: pass in the user supplied args for V (--valgrind-args=) and
+# use this to detect if a different tool has been selected. If so
+# adjust tool-specific args appropriately.
+#
+# TODO: pass in the path to the Valgrind to be used (--valgrind=), and
+# check what flags it accepts. Possible args that might be beneficial:
+#
+# --num-transtab-sectors=24 [reduces re-jitting overheads in long runs]
+# --px-default=allregs-at-mem-access
+# --px-file-backed=unwindregs-at-mem-access
+# [these reduce PX overheads as described above]
+#
+
+
+def get_default_valgrind_args():
+ return [
+ "--fair-sched=yes",
+ "--smc-check=all-non-file",
+ "--vex-iropt-register-updates=allregs-at-mem-access",
+ "--trace-children=yes",
+ "--child-silent-after-fork=yes",
+ (
+ "--trace-children-skip="
+ + "/usr/bin/hg,/bin/rm,*/bin/certutil,*/bin/pk12util,"
+ + "*/bin/ssltunnel,*/bin/uname,*/bin/which,*/bin/ps,"
+ + "*/bin/grep,*/bin/java,*/bin/lsb_release"
+ ),
+ ] + get_default_valgrind_tool_specific_args()
+
+
+# The default tool is Memcheck. Feeding these arguments to a different
+# Valgrind tool will cause it to fail at startup, so don't do that!
+
+
+def get_default_valgrind_tool_specific_args():
+ return [
+ "--partial-loads-ok=yes",
+ "--leak-check=summary",
+ "--show-possibly-lost=no",
+ "--show-mismatched-frees=no",
+ ]
diff --git a/testing/mozbase/mozdebug/setup.cfg b/testing/mozbase/mozdebug/setup.cfg
new file mode 100644
index 0000000000..3c6e79cf31
--- /dev/null
+++ b/testing/mozbase/mozdebug/setup.cfg
@@ -0,0 +1,2 @@
+[bdist_wheel]
+universal=1
diff --git a/testing/mozbase/mozdebug/setup.py b/testing/mozbase/mozdebug/setup.py
new file mode 100644
index 0000000000..109a79a26c
--- /dev/null
+++ b/testing/mozbase/mozdebug/setup.py
@@ -0,0 +1,33 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from __future__ import absolute_import
+
+from setuptools import setup
+
+PACKAGE_VERSION = "0.2.0"
+DEPS = ["mozinfo"]
+
+
+setup(
+ name="mozdebug",
+ version=PACKAGE_VERSION,
+ description="Utilities for running applications under native code debuggers "
+ "intended for use in Mozilla testing",
+ long_description="see https://firefox-source-docs.mozilla.org/mozbase/index.html",
+ classifiers=[
+ "Programming Language :: Python :: 2.7",
+ "Programming Language :: Python :: 2 :: Only",
+ ],
+ # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+ keywords="mozilla",
+ author="Mozilla Automation and Testing Team",
+ author_email="tools@lists.mozilla.org",
+ url="https://wiki.mozilla.org/Auto-tools/Projects/Mozbase",
+ license="MPL",
+ packages=["mozdebug"],
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=DEPS,
+)
diff --git a/testing/mozbase/mozdebug/tests/fake_debuggers/cgdb/cgdb b/testing/mozbase/mozdebug/tests/fake_debuggers/cgdb/cgdb
new file mode 100755
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/mozbase/mozdebug/tests/fake_debuggers/cgdb/cgdb
diff --git a/testing/mozbase/mozdebug/tests/fake_debuggers/devenv/devenv.exe b/testing/mozbase/mozdebug/tests/fake_debuggers/devenv/devenv.exe
new file mode 100755
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/mozbase/mozdebug/tests/fake_debuggers/devenv/devenv.exe
diff --git a/testing/mozbase/mozdebug/tests/fake_debuggers/gdb/gdb b/testing/mozbase/mozdebug/tests/fake_debuggers/gdb/gdb
new file mode 100755
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/mozbase/mozdebug/tests/fake_debuggers/gdb/gdb
diff --git a/testing/mozbase/mozdebug/tests/fake_debuggers/lldb/lldb b/testing/mozbase/mozdebug/tests/fake_debuggers/lldb/lldb
new file mode 100755
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/mozbase/mozdebug/tests/fake_debuggers/lldb/lldb
diff --git a/testing/mozbase/mozdebug/tests/fake_debuggers/wdexpress/wdexpress.exe b/testing/mozbase/mozdebug/tests/fake_debuggers/wdexpress/wdexpress.exe
new file mode 100755
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/mozbase/mozdebug/tests/fake_debuggers/wdexpress/wdexpress.exe
diff --git a/testing/mozbase/mozdebug/tests/manifest.ini b/testing/mozbase/mozdebug/tests/manifest.ini
new file mode 100644
index 0000000000..72aff7539a
--- /dev/null
+++ b/testing/mozbase/mozdebug/tests/manifest.ini
@@ -0,0 +1,3 @@
+[DEFAULT]
+subsuite = mozbase
+[test.py]
diff --git a/testing/mozbase/mozdebug/tests/test.py b/testing/mozbase/mozdebug/tests/test.py
new file mode 100644
index 0000000000..713671d228
--- /dev/null
+++ b/testing/mozbase/mozdebug/tests/test.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python
+
+from __future__ import absolute_import, print_function
+
+import os
+
+import mozunit
+import pytest
+from mozdebug.mozdebug import (
+ _DEBUGGER_PRIORITIES,
+ DebuggerSearch,
+ get_default_debugger_name,
+)
+
+
+here = os.path.abspath(os.path.dirname(__file__))
+
+
+@pytest.fixture
+def set_debuggers(monkeypatch):
+ debugger_dir = os.path.join(here, "fake_debuggers")
+
+ def _set_debuggers(*debuggers):
+ dirs = []
+ for d in debuggers:
+ if d.endswith(".exe"):
+ d = d[: -len(".exe")]
+ dirs.append(os.path.join(debugger_dir, d))
+ monkeypatch.setenv("PATH", os.pathsep.join(dirs))
+
+ return _set_debuggers
+
+
+@pytest.mark.parametrize("os_name", ["android", "linux", "mac", "win", "unknown"])
+def test_default_debugger_name(os_name, set_debuggers, monkeypatch):
+ import mozinfo
+ import sys
+
+ def update_os_name(*args, **kwargs):
+ mozinfo.info["os"] = os_name
+
+ monkeypatch.setattr(mozinfo, "find_and_update_from_json", update_os_name)
+
+ if sys.platform == "win32":
+ # This is used so distutils.spawn.find_executable doesn't add '.exe'
+ # suffixes to all our dummy binaries on Windows.
+ monkeypatch.setattr(sys, "platform", "linux")
+
+ debuggers = _DEBUGGER_PRIORITIES[os_name][:]
+ debuggers.reverse()
+ first = True
+ while len(debuggers) > 0:
+ set_debuggers(*debuggers)
+
+ if first:
+ assert get_default_debugger_name() == debuggers[-1]
+ first = False
+ else:
+ assert get_default_debugger_name() is None
+ assert (
+ get_default_debugger_name(DebuggerSearch.KeepLooking) == debuggers[-1]
+ )
+ debuggers = debuggers[:-1]
+
+
+if __name__ == "__main__":
+ mozunit.main()