Skip to content

Commit

Permalink
pythongh-109972: Enhance test_gdb
Browse files Browse the repository at this point in the history
* Split PyBtTests.test_pycfunction() into 2 files, test_cfunction and
  test_cfunction_full, and 6 functions:

  * test_pycfunction_noargs()
  * test_pycfunction_o()
  * test_pycfunction_varargs()
  * test_pycfunction_varargs_keywords()
  * test_pycfunction_fastcall()
  * test_pycfunction_fastcall_keywords()

* In verbose mode, these "pycfunction" tests now log each tested
  call.
* Move get_gdb_repr() to PrettyPrintTests.
* Replace DebuggerTests.get_sample_script() with SAMPLE_SCRIPT.
* Rename checkout_hook_path to CHECKOUT_HOOK_PATH.
* Rename gdb_version to GDB_VERSION_TEXT.
* Replace (gdb_major_version, gdb_minor_version) with GDB_VERSION.
* run_gdb() uses "backslashreplace" error handler.
* Add check_gdb() function to util.py.
  • Loading branch information
vstinner committed Sep 28, 2023
1 parent 9be283e commit d79ce89
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 211 deletions.
6 changes: 3 additions & 3 deletions Lib/test/test_gdb/test_backtrace.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
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():
Expand All @@ -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'''^.*
Expand All @@ -35,7 +35,7 @@ def test_bt(self):
"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'''^.*
Expand Down
114 changes: 58 additions & 56 deletions Lib/test/test_gdb/test_cfunction.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand All @@ -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', '')
36 changes: 36 additions & 0 deletions Lib/test/test_gdb/test_cfunction_full.py
Original file line number Diff line number Diff line change
@@ -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
20 changes: 10 additions & 10 deletions Lib/test/test_gdb/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
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():
Expand Down Expand Up @@ -32,7 +32,7 @@ def assertListing(self, expected, actual):

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'
Expand All @@ -47,7 +47,7 @@ def test_basic_command(self):

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'
Expand All @@ -58,7 +58,7 @@ def test_one_abs_arg(self):

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'
Expand Down Expand Up @@ -101,15 +101,15 @@ def test_pyup_command(self):
@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')

@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')
Expand Down Expand Up @@ -150,15 +150,15 @@ def test_print_after_up(self):
@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.*")

@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.*")
Expand All @@ -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.*")
Expand All @@ -176,7 +176,7 @@ def test_basic_command(self):
@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'''^.*
Expand Down
54 changes: 46 additions & 8 deletions Lib/test/test_gdb/test_pretty_print.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from test import support

from .util import (
BREAKPOINT_FN, gdb_major_version, gdb_minor_version,
BREAKPOINT_FN, GDB_VERSION,
run_gdb, setup_module, DebuggerTests)


Expand All @@ -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)
Expand Down Expand Up @@ -75,15 +111,17 @@ def test_strings(self):
# 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:
Expand Down Expand Up @@ -122,7 +160,7 @@ def test_tuples(self):
@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'}")
Expand All @@ -141,7 +179,7 @@ def test_sets(self):
@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'})")
Expand Down
Loading

0 comments on commit d79ce89

Please sign in to comment.