summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Lib/test/support/__init__.py7
-rw-r--r--Lib/test/test_gdb/__init__.py24
-rw-r--r--Lib/test/test_gdb/test_backtrace.py6
-rw-r--r--Lib/test/test_gdb/test_cfunction.py114
-rw-r--r--Lib/test/test_gdb/test_cfunction_full.py36
-rw-r--r--Lib/test/test_gdb/test_misc.py20
-rw-r--r--Lib/test/test_gdb/test_pretty_print.py54
-rw-r--r--Lib/test/test_gdb/util.py256
8 files changed, 299 insertions, 218 deletions
diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py
index 4fcb899..38d5012 100644
--- a/Lib/test/support/__init__.py
+++ b/Lib/test/support/__init__.py
@@ -777,14 +777,17 @@ def check_cflags_pgo():
# Check if Python was built with ./configure --enable-optimizations:
# with Profile Guided Optimization (PGO).
cflags_nodist = sysconfig.get_config_var('PY_CFLAGS_NODIST') or ''
- pgo_options = (
+ pgo_options = [
# GCC
'-fprofile-use',
# clang: -fprofile-instr-use=code.profclangd
'-fprofile-instr-use',
# ICC
"-prof-use",
- )
+ ]
+ PGO_PROF_USE_FLAG = sysconfig.get_config_var('PGO_PROF_USE_FLAG')
+ if PGO_PROF_USE_FLAG:
+ pgo_options.append(PGO_PROF_USE_FLAG)
return any(option in cflags_nodist for option in pgo_options)
diff --git a/Lib/test/test_gdb/__init__.py b/Lib/test/test_gdb/__init__.py
index 0261f59..d74075e 100644
--- a/Lib/test/test_gdb/__init__.py
+++ b/Lib/test/test_gdb/__init__.py
@@ -4,7 +4,27 @@
# Lib/test/test_jit_gdb.py
import os
-from test.support import load_package_tests
+import sysconfig
+import unittest
+from test import support
+
+
+MS_WINDOWS = (os.name == 'nt')
+if MS_WINDOWS:
+ # On Windows, Python is usually built by MSVC. Passing /p:DebugSymbols=true
+ # option to MSBuild produces PDB debug symbols, but gdb doesn't support PDB
+ # debug symbol files.
+ raise unittest.SkipTest("test_gdb doesn't work on Windows")
+
+if support.PGO:
+ raise unittest.SkipTest("test_gdb is not useful for PGO")
+
+if not sysconfig.is_python_build():
+ raise unittest.SkipTest("test_gdb only works on source builds at the moment.")
+
+if support.check_cflags_pgo():
+ raise unittest.SkipTest("test_gdb is not reliable on PGO builds")
+
def load_tests(*args):
- return load_package_tests(os.path.dirname(__file__), *args)
+ return support.load_package_tests(os.path.dirname(__file__), *args)
diff --git a/Lib/test/test_gdb/test_backtrace.py b/Lib/test/test_gdb/test_backtrace.py
index 15cbcf1..c41e7cb 100644
--- a/Lib/test/test_gdb/test_backtrace.py
+++ b/Lib/test/test_gdb/test_backtrace.py
@@ -3,7 +3,7 @@ import unittest
from test import support
from test.support import python_is_optimized
-from .util import setup_module, DebuggerTests, CET_PROTECTION
+from .util import setup_module, DebuggerTests, CET_PROTECTION, SAMPLE_SCRIPT
def setUpModule():
@@ -15,7 +15,7 @@ class PyBtTests(DebuggerTests):
"Python was compiled with optimizations")
def test_bt(self):
'Verify that the "py-bt" command works'
- bt = self.get_stack_trace(script=self.get_sample_script(),
+ bt = self.get_stack_trace(script=SAMPLE_SCRIPT,
cmds_after_breakpoint=['py-bt'])
self.assertMultilineMatches(bt,
r'''^.*
@@ -35,7 +35,7 @@ Traceback \(most recent call first\):
"Python was compiled with optimizations")
def test_bt_full(self):
'Verify that the "py-bt-full" command works'
- bt = self.get_stack_trace(script=self.get_sample_script(),
+ bt = self.get_stack_trace(script=SAMPLE_SCRIPT,
cmds_after_breakpoint=['py-bt-full'])
self.assertMultilineMatches(bt,
r'''^.*
diff --git a/Lib/test/test_gdb/test_cfunction.py b/Lib/test/test_gdb/test_cfunction.py
index 55796d0..0a62014 100644
--- a/Lib/test/test_gdb/test_cfunction.py
+++ b/Lib/test/test_gdb/test_cfunction.py
@@ -1,8 +1,6 @@
-import re
import textwrap
import unittest
from test import support
-from test.support import python_is_optimized
from .util import setup_module, DebuggerTests
@@ -11,10 +9,22 @@ def setUpModule():
setup_module()
-@unittest.skipIf(python_is_optimized(),
+@unittest.skipIf(support.python_is_optimized(),
"Python was compiled with optimizations")
@support.requires_resource('cpu')
class CFunctionTests(DebuggerTests):
+ def check(self, func_name, cmd):
+ # Verify with "py-bt":
+ gdb_output = self.get_stack_trace(
+ cmd,
+ breakpoint=func_name,
+ cmds_after_breakpoint=['bt', 'py-bt'],
+ # bpo-45207: Ignore 'Function "meth_varargs" not
+ # defined.' message in stderr.
+ ignore_stderr=True,
+ )
+ self.assertIn(f'<built-in method {func_name}', gdb_output)
+
# Some older versions of gdb will fail with
# "Cannot find new threads: generic error"
# unless we add LD_PRELOAD=PATH-TO-libpthread.so.1 as a workaround
@@ -24,60 +34,52 @@ class CFunctionTests(DebuggerTests):
# This is because we are calling functions from an "external" module
# (_testcapimodule) rather than compiled-in functions. It seems difficult
# to suppress these. See also the comment in DebuggerTests.get_stack_trace
- def test_pycfunction(self):
+ def check_pycfunction(self, func_name, args):
'Verify that "py-bt" displays invocations of PyCFunction instances'
- # bpo-46600: If the compiler inlines _null_to_none() in meth_varargs()
- # (ex: clang -Og), _null_to_none() is the frame #1. Otherwise,
- # meth_varargs() is the frame #1.
- expected_frame = r'#(1|2)'
+
+ if support.verbose:
+ print()
+
# Various optimizations multiply the code paths by which these are
# called, so test a variety of calling conventions.
- for func_name, args in (
- ('meth_varargs', ''),
- ('meth_varargs_keywords', ''),
- ('meth_o', '[]'),
- ('meth_noargs', ''),
- ('meth_fastcall', ''),
- ('meth_fastcall_keywords', ''),
+ for obj in (
+ '_testcapi',
+ '_testcapi.MethClass',
+ '_testcapi.MethClass()',
+ '_testcapi.MethStatic()',
+
+ # XXX: bound methods don't yet give nice tracebacks
+ # '_testcapi.MethInstance()',
):
- for obj in (
- '_testcapi',
- '_testcapi.MethClass',
- '_testcapi.MethClass()',
- '_testcapi.MethStatic()',
-
- # XXX: bound methods don't yet give nice tracebacks
- # '_testcapi.MethInstance()',
- ):
- with self.subTest(f'{obj}.{func_name}'):
- cmd = textwrap.dedent(f'''
- import _testcapi
- def foo():
- {obj}.{func_name}({args})
- def bar():
- foo()
- bar()
- ''')
- # Verify with "py-bt":
- gdb_output = self.get_stack_trace(
- cmd,
- breakpoint=func_name,
- cmds_after_breakpoint=['bt', 'py-bt'],
- # bpo-45207: Ignore 'Function "meth_varargs" not
- # defined.' message in stderr.
- ignore_stderr=True,
- )
- self.assertIn(f'<built-in method {func_name}', gdb_output)
-
- # Verify with "py-bt-full":
- gdb_output = self.get_stack_trace(
- cmd,
- breakpoint=func_name,
- cmds_after_breakpoint=['py-bt-full'],
- # bpo-45207: Ignore 'Function "meth_varargs" not
- # defined.' message in stderr.
- ignore_stderr=True,
- )
- regex = expected_frame
- regex += re.escape(f' <built-in method {func_name}')
- self.assertRegex(gdb_output, regex)
+ with self.subTest(f'{obj}.{func_name}'):
+ call = f'{obj}.{func_name}({args})'
+ cmd = textwrap.dedent(f'''
+ import _testcapi
+ def foo():
+ {call}
+ def bar():
+ foo()
+ bar()
+ ''')
+ if support.verbose:
+ print(f' test call: {call}', flush=True)
+
+ self.check(func_name, cmd)
+
+ def test_pycfunction_noargs(self):
+ self.check_pycfunction('meth_noargs', '')
+
+ def test_pycfunction_o(self):
+ self.check_pycfunction('meth_o', '[]')
+
+ def test_pycfunction_varargs(self):
+ self.check_pycfunction('meth_varargs', '')
+
+ def test_pycfunction_varargs_keywords(self):
+ self.check_pycfunction('meth_varargs_keywords', '')
+
+ def test_pycfunction_fastcall(self):
+ self.check_pycfunction('meth_fastcall', '')
+
+ def test_pycfunction_fastcall_keywords(self):
+ self.check_pycfunction('meth_fastcall_keywords', '')
diff --git a/Lib/test/test_gdb/test_cfunction_full.py b/Lib/test/test_gdb/test_cfunction_full.py
new file mode 100644
index 0000000..3e90cb1
--- /dev/null
+++ b/Lib/test/test_gdb/test_cfunction_full.py
@@ -0,0 +1,36 @@
+"""
+Similar to test_cfunction but test "py-bt-full" command.
+"""
+
+import re
+
+from .util import setup_module
+from .test_cfunction import CFunctionTests
+
+
+def setUpModule():
+ setup_module()
+
+
+class CFunctionFullTests(CFunctionTests):
+ def check(self, func_name, cmd):
+ # Verify with "py-bt-full":
+ gdb_output = self.get_stack_trace(
+ cmd,
+ breakpoint=func_name,
+ cmds_after_breakpoint=['py-bt-full'],
+ # bpo-45207: Ignore 'Function "meth_varargs" not
+ # defined.' message in stderr.
+ ignore_stderr=True,
+ )
+
+ # bpo-46600: If the compiler inlines _null_to_none() in
+ # meth_varargs() (ex: clang -Og), _null_to_none() is the
+ # frame #1. Otherwise, meth_varargs() is the frame #1.
+ regex = r'#(1|2)'
+ regex += re.escape(f' <built-in method {func_name}')
+ self.assertRegex(gdb_output, regex)
+
+
+# Delete the test case, otherwise it's executed twice
+del CFunctionTests
diff --git a/Lib/test/test_gdb/test_misc.py b/Lib/test/test_gdb/test_misc.py
index 1063e7b..1047f48 100644
--- a/Lib/test/test_gdb/test_misc.py
+++ b/Lib/test/test_gdb/test_misc.py
@@ -2,7 +2,7 @@ import re
import unittest
from test.support import python_is_optimized
-from .util import run_gdb, setup_module, DebuggerTests
+from .util import run_gdb, setup_module, DebuggerTests, SAMPLE_SCRIPT
def setUpModule():
@@ -32,7 +32,7 @@ class PyListTests(DebuggerTests):
def test_basic_command(self):
'Verify that the "py-list" command works'
- bt = self.get_stack_trace(script=self.get_sample_script(),
+ bt = self.get_stack_trace(script=SAMPLE_SCRIPT,
cmds_after_breakpoint=['py-list'])
self.assertListing(' 5 \n'
@@ -47,7 +47,7 @@ class PyListTests(DebuggerTests):
def test_one_abs_arg(self):
'Verify the "py-list" command with one absolute argument'
- bt = self.get_stack_trace(script=self.get_sample_script(),
+ bt = self.get_stack_trace(script=SAMPLE_SCRIPT,
cmds_after_breakpoint=['py-list 9'])
self.assertListing(' 9 def baz(*args):\n'
@@ -58,7 +58,7 @@ class PyListTests(DebuggerTests):
def test_two_abs_args(self):
'Verify the "py-list" command with two absolute arguments'
- bt = self.get_stack_trace(script=self.get_sample_script(),
+ bt = self.get_stack_trace(script=SAMPLE_SCRIPT,
cmds_after_breakpoint=['py-list 1,3'])
self.assertListing(' 1 # Sample script for use by test_gdb\n'
@@ -101,7 +101,7 @@ $''')
@unittest.skipUnless(HAS_PYUP_PYDOWN, "test requires py-up/py-down commands")
def test_down_at_bottom(self):
'Verify handling of "py-down" at the bottom of the stack'
- bt = self.get_stack_trace(script=self.get_sample_script(),
+ bt = self.get_stack_trace(script=SAMPLE_SCRIPT,
cmds_after_breakpoint=['py-down'])
self.assertEndsWith(bt,
'Unable to find a newer python frame\n')
@@ -109,7 +109,7 @@ $''')
@unittest.skipUnless(HAS_PYUP_PYDOWN, "test requires py-up/py-down commands")
def test_up_at_top(self):
'Verify handling of "py-up" at the top of the stack'
- bt = self.get_stack_trace(script=self.get_sample_script(),
+ bt = self.get_stack_trace(script=SAMPLE_SCRIPT,
cmds_after_breakpoint=['py-up'] * 5)
self.assertEndsWith(bt,
'Unable to find an older python frame\n')
@@ -150,7 +150,7 @@ class PyPrintTests(DebuggerTests):
@unittest.skipIf(python_is_optimized(),
"Python was compiled with optimizations")
def test_printing_global(self):
- bt = self.get_stack_trace(script=self.get_sample_script(),
+ bt = self.get_stack_trace(script=SAMPLE_SCRIPT,
cmds_after_breakpoint=['py-up', 'py-print __name__'])
self.assertMultilineMatches(bt,
r".*\nglobal '__name__' = '__main__'\n.*")
@@ -158,7 +158,7 @@ class PyPrintTests(DebuggerTests):
@unittest.skipIf(python_is_optimized(),
"Python was compiled with optimizations")
def test_printing_builtin(self):
- bt = self.get_stack_trace(script=self.get_sample_script(),
+ bt = self.get_stack_trace(script=SAMPLE_SCRIPT,
cmds_after_breakpoint=['py-up', 'py-print len'])
self.assertMultilineMatches(bt,
r".*\nbuiltin 'len' = <built-in method len of module object at remote 0x-?[0-9a-f]+>\n.*")
@@ -167,7 +167,7 @@ class PyLocalsTests(DebuggerTests):
@unittest.skipIf(python_is_optimized(),
"Python was compiled with optimizations")
def test_basic_command(self):
- bt = self.get_stack_trace(script=self.get_sample_script(),
+ bt = self.get_stack_trace(script=SAMPLE_SCRIPT,
cmds_after_breakpoint=['py-up', 'py-locals'])
self.assertMultilineMatches(bt,
r".*\nargs = \(1, 2, 3\)\n.*")
@@ -176,7 +176,7 @@ class PyLocalsTests(DebuggerTests):
@unittest.skipIf(python_is_optimized(),
"Python was compiled with optimizations")
def test_locals_after_up(self):
- bt = self.get_stack_trace(script=self.get_sample_script(),
+ bt = self.get_stack_trace(script=SAMPLE_SCRIPT,
cmds_after_breakpoint=['py-up', 'py-up', 'py-locals'])
self.assertMultilineMatches(bt,
r'''^.*
diff --git a/Lib/test/test_gdb/test_pretty_print.py b/Lib/test/test_gdb/test_pretty_print.py
index e31dc66..dfc77d6 100644
--- a/Lib/test/test_gdb/test_pretty_print.py
+++ b/Lib/test/test_gdb/test_pretty_print.py
@@ -3,7 +3,7 @@ import sys
from test import support
from .util import (
- BREAKPOINT_FN, gdb_major_version, gdb_minor_version,
+ BREAKPOINT_FN, GDB_VERSION,
run_gdb, setup_module, DebuggerTests)
@@ -12,6 +12,42 @@ def setUpModule():
class PrettyPrintTests(DebuggerTests):
+ def get_gdb_repr(self, source,
+ cmds_after_breakpoint=None,
+ import_site=False):
+ # Given an input python source representation of data,
+ # run "python -c'id(DATA)'" under gdb with a breakpoint on
+ # builtin_id and scrape out gdb's representation of the "op"
+ # parameter, and verify that the gdb displays the same string
+ #
+ # Verify that the gdb displays the expected string
+ #
+ # For a nested structure, the first time we hit the breakpoint will
+ # give us the top-level structure
+
+ # NOTE: avoid decoding too much of the traceback as some
+ # undecodable characters may lurk there in optimized mode
+ # (issue #19743).
+ cmds_after_breakpoint = cmds_after_breakpoint or ["backtrace 1"]
+ gdb_output = self.get_stack_trace(source, breakpoint=BREAKPOINT_FN,
+ cmds_after_breakpoint=cmds_after_breakpoint,
+ import_site=import_site)
+ # gdb can insert additional '\n' and space characters in various places
+ # in its output, depending on the width of the terminal it's connected
+ # to (using its "wrap_here" function)
+ m = re.search(
+ # Match '#0 builtin_id(self=..., v=...)'
+ r'#0\s+builtin_id\s+\(self\=.*,\s+v=\s*(.*?)?\)'
+ # Match ' at Python/bltinmodule.c'.
+ # bpo-38239: builtin_id() is defined in Python/bltinmodule.c,
+ # but accept any "Directory\file.c" to support Link Time
+ # Optimization (LTO).
+ r'\s+at\s+\S*[A-Za-z]+/[A-Za-z0-9_-]+\.c',
+ gdb_output, re.DOTALL)
+ if not m:
+ self.fail('Unexpected gdb output: %r\n%s' % (gdb_output, gdb_output))
+ return m.group(1), gdb_output
+
def test_getting_backtrace(self):
gdb_output = self.get_stack_trace('id(42)')
self.assertTrue(BREAKPOINT_FN in gdb_output)
@@ -75,15 +111,17 @@ class PrettyPrintTests(DebuggerTests):
# as GDB might have been linked against a different version
# of Python with a different encoding and coercion policy
# with respect to PEP 538 and PEP 540.
- out, err = run_gdb(
+ stdout, stderr = run_gdb(
'--eval-command',
'python import locale; print(locale.getpreferredencoding())')
- encoding = out.rstrip()
- if err or not encoding:
+ encoding = stdout
+ if stderr or not encoding:
raise RuntimeError(
- f'unable to determine the preferred encoding '
- f'of embedded Python in GDB: {err}')
+ f'unable to determine the Python locale preferred encoding '
+ f'of embedded Python in GDB\n'
+ f'stdout={stdout!r}\n'
+ f'stderr={stderr!r}')
def check_repr(text):
try:
@@ -122,7 +160,7 @@ class PrettyPrintTests(DebuggerTests):
@support.requires_resource('cpu')
def test_sets(self):
'Verify the pretty-printing of sets'
- if (gdb_major_version, gdb_minor_version) < (7, 3):
+ if GDB_VERSION < (7, 3):
self.skipTest("pretty-printing of sets needs gdb 7.3 or later")
self.assertGdbRepr(set(), "set()")
self.assertGdbRepr(set(['a']), "{'a'}")
@@ -141,7 +179,7 @@ id(s)''')
@support.requires_resource('cpu')
def test_frozensets(self):
'Verify the pretty-printing of frozensets'
- if (gdb_major_version, gdb_minor_version) < (7, 3):
+ if GDB_VERSION < (7, 3):
self.skipTest("pretty-printing of frozensets needs gdb 7.3 or later")
self.assertGdbRepr(frozenset(), "frozenset()")
self.assertGdbRepr(frozenset(['a']), "frozenset({'a'})")
diff --git a/Lib/test/test_gdb/util.py b/Lib/test/test_gdb/util.py
index 30beb4e..7f4e3cb 100644
--- a/Lib/test/test_gdb/util.py
+++ b/Lib/test/test_gdb/util.py
@@ -1,5 +1,6 @@
import os
import re
+import shlex
import subprocess
import sys
import sysconfig
@@ -7,29 +8,74 @@ import unittest
from test import support
-MS_WINDOWS = (sys.platform == 'win32')
-if MS_WINDOWS:
- raise unittest.SkipTest("test_gdb doesn't work on Windows")
+# Location of custom hooks file in a repository checkout.
+CHECKOUT_HOOK_PATH = os.path.join(os.path.dirname(sys.executable),
+ 'python-gdb.py')
+
+SAMPLE_SCRIPT = os.path.join(os.path.dirname(__file__), 'gdb_sample.py')
+BREAKPOINT_FN = 'builtin_id'
+
+PYTHONHASHSEED = '123'
+
+
+def clean_environment():
+ # Remove PYTHON* environment variables such as PYTHONHOME
+ return {name: value for name, value in os.environ.items()
+ if not name.startswith('PYTHON')}
+
+
+# Temporary value until it's initialized by get_gdb_version() below
+GDB_VERSION = (0, 0)
+
+def run_gdb(*args, exitcode=0, **env_vars):
+ """Runs gdb in --batch mode with the additional arguments given by *args.
+
+ Returns its (stdout, stderr) decoded from utf-8 using the replace handler.
+ """
+ env = clean_environment()
+ if env_vars:
+ env.update(env_vars)
+
+ cmd = ['gdb',
+ # Batch mode: Exit after processing all the command files
+ # specified with -x/--command
+ '--batch',
+ # -nx: Do not execute commands from any .gdbinit initialization
+ # files (gh-66384)
+ '-nx']
+ if GDB_VERSION >= (7, 4):
+ cmd.extend(('--init-eval-command',
+ f'add-auto-load-safe-path {CHECKOUT_HOOK_PATH}'))
+ cmd.extend(args)
+
+ proc = subprocess.run(
+ cmd,
+ # Redirect stdin to prevent gdb from messing with the terminal settings
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ encoding="utf8", errors="backslashreplace",
+ env=env)
+
+ stdout = proc.stdout
+ stderr = proc.stderr
+ if proc.returncode != exitcode:
+ cmd_text = shlex.join(cmd)
+ raise Exception(f"{cmd_text} failed with exit code {proc.returncode}, "
+ f"expected exit code {exitcode}:\n"
+ f"stdout={stdout!r}\n"
+ f"stderr={stderr!r}")
+
+ return (stdout, stderr)
def get_gdb_version():
try:
- cmd = ["gdb", "-nx", "--version"]
- proc = subprocess.Popen(cmd,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- universal_newlines=True)
- with proc:
- version, stderr = proc.communicate()
-
- if proc.returncode:
- raise Exception(f"Command {' '.join(cmd)!r} failed "
- f"with exit code {proc.returncode}: "
- f"stdout={version!r} stderr={stderr!r}")
+ stdout, stderr = run_gdb('--version')
except OSError:
# This is what "no gdb" looks like. There may, however, be other
# errors that manifest this way too.
- raise unittest.SkipTest("Couldn't find gdb on the path")
+ raise unittest.SkipTest("Couldn't find gdb program on the path")
# Regex to parse:
# 'GNU gdb (GDB; SUSE Linux Enterprise 12) 7.7\n' -> 7.7
@@ -37,32 +83,48 @@ def get_gdb_version():
# 'GNU gdb 6.1.1 [FreeBSD]\n' -> 6.1
# 'GNU gdb (GDB) Fedora (7.5.1-37.fc18)\n' -> 7.5
# 'HP gdb 6.7 for HP Itanium (32 or 64 bit) and target HP-UX 11iv2 and 11iv3.\n' -> 6.7
- match = re.search(r"^(?:GNU|HP) gdb.*?\b(\d+)\.(\d+)", version)
+ match = re.search(r"^(?:GNU|HP) gdb.*?\b(\d+)\.(\d+)", stdout)
if match is None:
- raise Exception("unable to parse GDB version: %r" % version)
- return (version, int(match.group(1)), int(match.group(2)))
+ raise Exception("unable to parse gdb version: %r" % stdout)
+ version_text = stdout
+ major = int(match.group(1))
+ minor = int(match.group(2))
+ version = (major, minor)
+ return (version_text, version)
-gdb_version, gdb_major_version, gdb_minor_version = get_gdb_version()
-if gdb_major_version < 7:
- raise unittest.SkipTest("gdb versions before 7.0 didn't support python "
- "embedding. Saw %s.%s:\n%s"
- % (gdb_major_version, gdb_minor_version,
- gdb_version))
+GDB_VERSION_TEXT, GDB_VERSION = get_gdb_version()
+if GDB_VERSION < (7, 0):
+ raise unittest.SkipTest(
+ f"gdb versions before 7.0 didn't support python embedding. "
+ f"Saw gdb version {GDB_VERSION[0]}.{GDB_VERSION[1]}:\n"
+ f"{GDB_VERSION_TEXT}")
-if not sysconfig.is_python_build():
- raise unittest.SkipTest("test_gdb only works on source builds at the moment.")
-if ((sysconfig.get_config_var('PGO_PROF_USE_FLAG') or 'xxx') in
- (sysconfig.get_config_var('PY_CORE_CFLAGS') or '')):
- raise unittest.SkipTest("test_gdb is not reliable on PGO builds")
+def check_usable_gdb():
+ # Verify that "gdb" was built with the embedded Python support enabled and
+ # verify that "gdb" can load our custom hooks, as OS security settings may
+ # disallow this without a customized .gdbinit.
+ stdout, stderr = run_gdb(
+ '--eval-command=python import sys; print(sys.version_info)',
+ '--args', sys.executable)
-# Location of custom hooks file in a repository checkout.
-checkout_hook_path = os.path.join(os.path.dirname(sys.executable),
- 'python-gdb.py')
+ if "auto-loading has been declined" in stderr:
+ raise unittest.SkipTest(
+ f"gdb security settings prevent use of custom hooks; "
+ f"stderr: {stderr!r}")
-PYTHONHASHSEED = '123'
+ if not stdout:
+ raise unittest.SkipTest(
+ f"gdb not built with embedded python support; "
+ f"stderr: {stderr!r}")
+
+ if "major=2" in stdout:
+ raise unittest.SkipTest("gdb built with Python 2")
+check_usable_gdb()
+
+# Control-flow enforcement technology
def cet_protection():
cflags = sysconfig.get_config_var('CFLAGS')
if not cflags:
@@ -74,63 +136,17 @@ def cet_protection():
and any((flag.startswith('-fcf-protection')
and not flag.endswith(("=none", "=return")))
for flag in flags))
-
-# Control-flow enforcement technology
CET_PROTECTION = cet_protection()
-def run_gdb(*args, **env_vars):
- """Runs gdb in --batch mode with the additional arguments given by *args.
-
- Returns its (stdout, stderr) decoded from utf-8 using the replace handler.
- """
- if env_vars:
- env = os.environ.copy()
- env.update(env_vars)
- else:
- env = None
- # -nx: Do not execute commands from any .gdbinit initialization files
- # (issue #22188)
- base_cmd = ('gdb', '--batch', '-nx')
- if (gdb_major_version, gdb_minor_version) >= (7, 4):
- base_cmd += ('-iex', 'add-auto-load-safe-path ' + checkout_hook_path)
- proc = subprocess.Popen(base_cmd + args,
- # Redirect stdin to prevent GDB from messing with
- # the terminal settings
- stdin=subprocess.PIPE,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- env=env)
- with proc:
- out, err = proc.communicate()
- return out.decode('utf-8', 'replace'), err.decode('utf-8', 'replace')
-
-# Verify that "gdb" was built with the embedded python support enabled:
-gdbpy_version, _ = run_gdb("--eval-command=python import sys; print(sys.version_info)")
-if not gdbpy_version:
- raise unittest.SkipTest("gdb not built with embedded python support")
-
-if "major=2" in gdbpy_version:
- raise unittest.SkipTest("gdb built with Python 2")
-
-# Verify that "gdb" can load our custom hooks, as OS security settings may
-# disallow this without a customized .gdbinit.
-_, gdbpy_errors = run_gdb('--args', sys.executable)
-if "auto-loading has been declined" in gdbpy_errors:
- msg = "gdb security settings prevent use of custom hooks: "
- raise unittest.SkipTest(msg + gdbpy_errors.rstrip())
-
-BREAKPOINT_FN='builtin_id'
-
-
def setup_module():
if support.verbose:
- print("GDB version %s.%s:" % (gdb_major_version, gdb_minor_version))
- for line in gdb_version.splitlines():
+ print(f"gdb version {GDB_VERSION[0]}.{GDB_VERSION[1]}:")
+ for line in GDB_VERSION_TEXT.splitlines():
print(" " * 4 + line)
+ print()
-@unittest.skipIf(support.PGO, "not useful for PGO")
class DebuggerTests(unittest.TestCase):
"""Test that the debugger can debug Python."""
@@ -163,20 +179,22 @@ class DebuggerTests(unittest.TestCase):
# structures
# Generate a list of commands in gdb's language:
- commands = ['set breakpoint pending yes',
- 'break %s' % breakpoint,
-
- # The tests assume that the first frame of printed
- # backtrace will not contain program counter,
- # that is however not guaranteed by gdb
- # therefore we need to use 'set print address off' to
- # make sure the counter is not there. For example:
- # #0 in PyObject_Print ...
- # is assumed, but sometimes this can be e.g.
- # #0 0x00003fffb7dd1798 in PyObject_Print ...
- 'set print address off',
-
- 'run']
+ commands = [
+ 'set breakpoint pending yes',
+ 'break %s' % breakpoint,
+
+ # The tests assume that the first frame of printed
+ # backtrace will not contain program counter,
+ # that is however not guaranteed by gdb
+ # therefore we need to use 'set print address off' to
+ # make sure the counter is not there. For example:
+ # #0 in PyObject_Print ...
+ # is assumed, but sometimes this can be e.g.
+ # #0 0x00003fffb7dd1798 in PyObject_Print ...
+ 'set print address off',
+
+ 'run',
+ ]
# GDB as of 7.4 onwards can distinguish between the
# value of a variable at entry vs current value:
@@ -184,7 +202,7 @@ class DebuggerTests(unittest.TestCase):
# which leads to the selftests failing with errors like this:
# AssertionError: 'v@entry=()' != '()'
# Disable this:
- if (gdb_major_version, gdb_minor_version) >= (7, 4):
+ if GDB_VERSION >= (7, 4):
commands += ['set print entry-values no']
if cmds_after_breakpoint:
@@ -237,13 +255,16 @@ class DebuggerTests(unittest.TestCase):
for pattern in (
'(frame information optimized out)',
'Unable to read information on python frame',
+
# gh-91960: On Python built with "clang -Og", gdb gets
# "frame=<optimized out>" for _PyEval_EvalFrameDefault() parameter
'(unable to read python frame information)',
+
# gh-104736: On Python built with "clang -Og" on ppc64le,
# "py-bt" displays a truncated or not traceback, but "where"
# logs this error message:
'Backtrace stopped: frame did not save the PC',
+
# gh-104736: When "bt" command displays something like:
# "#1 0x0000000000000000 in ?? ()", the traceback is likely
# truncated or wrong.
@@ -254,42 +275,6 @@ class DebuggerTests(unittest.TestCase):
return out
- def get_gdb_repr(self, source,
- cmds_after_breakpoint=None,
- import_site=False):
- # Given an input python source representation of data,
- # run "python -c'id(DATA)'" under gdb with a breakpoint on
- # builtin_id and scrape out gdb's representation of the "op"
- # parameter, and verify that the gdb displays the same string
- #
- # Verify that the gdb displays the expected string
- #
- # For a nested structure, the first time we hit the breakpoint will
- # give us the top-level structure
-
- # NOTE: avoid decoding too much of the traceback as some
- # undecodable characters may lurk there in optimized mode
- # (issue #19743).
- cmds_after_breakpoint = cmds_after_breakpoint or ["backtrace 1"]
- gdb_output = self.get_stack_trace(source, breakpoint=BREAKPOINT_FN,
- cmds_after_breakpoint=cmds_after_breakpoint,
- import_site=import_site)
- # gdb can insert additional '\n' and space characters in various places
- # in its output, depending on the width of the terminal it's connected
- # to (using its "wrap_here" function)
- m = re.search(
- # Match '#0 builtin_id(self=..., v=...)'
- r'#0\s+builtin_id\s+\(self\=.*,\s+v=\s*(.*?)?\)'
- # Match ' at Python/bltinmodule.c'.
- # bpo-38239: builtin_id() is defined in Python/bltinmodule.c,
- # but accept any "Directory\file.c" to support Link Time
- # Optimization (LTO).
- r'\s+at\s+\S*[A-Za-z]+/[A-Za-z0-9_-]+\.c',
- gdb_output, re.DOTALL)
- if not m:
- self.fail('Unexpected gdb output: %r\n%s' % (gdb_output, gdb_output))
- return m.group(1), gdb_output
-
def assertEndsWith(self, actual, exp_end):
'''Ensure that the given "actual" string ends with "exp_end"'''
self.assertTrue(actual.endswith(exp_end),
@@ -299,6 +284,3 @@ class DebuggerTests(unittest.TestCase):
m = re.match(pattern, actual, re.DOTALL)
if not m:
self.fail(msg='%r did not match %r' % (actual, pattern))
-
- def get_sample_script(self):
- return os.path.join(os.path.dirname(__file__), 'gdb_sample.py')