Skip to content

Commit

Permalink
[3.12] gh-109972: Enhance test_gdb (#110026) (#110351)
Browse files Browse the repository at this point in the history
* gh-109972: Enhance test_gdb (#110026)

* Split test_pycfunction.py: add test_cfunction_full.py.
  Split the function into the following 6 functions. In verbose
  mode, these "pycfunction" tests now log each tested call.

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

* 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 instead of "replace".
* Add check_gdb() function to util.py.
* Enhance support.check_cflags_pgo(): check also for sysconfig
  PGO_PROF_USE_FLAG (if available) in compiler flags.
* Move some SkipTest checks to test_gdb/__init__.py.
* Elaborate why gdb cannot be tested on Windows: gdb doesn't support
  PDB debug symbol files.

(cherry picked from commit 757cbd4)

* gh-104736: Fix test_gdb tests on ppc64le with clang (#109360)

Fix test_gdb on Python built with LLVM clang 16 on Linux ppc64le (ex:
Fedora 38). Search patterns in gdb "bt" command output to detect
when gdb fails to retrieve the traceback. For example, skip a test if
"Backtrace stopped: frame did not save the PC" is found.

(cherry picked from commit 44d9a71)

* gh-110166: Fix gdb CFunctionFullTests on ppc64le clang build (#110331)

CFunctionFullTests now also runs "bt" command before "py-bt-full",
similar to CFunctionTests which also runs "bt" command before
"py-bt". So test_gdb can skip the test if patterns like "?? ()" are
found in the gdb output.

(cherry picked from commit 1de9406)
  • Loading branch information
vstinner authored Oct 4, 2023
1 parent 1d032ea commit bbce8bd
Show file tree
Hide file tree
Showing 9 changed files with 303 additions and 218 deletions.
7 changes: 5 additions & 2 deletions Lib/test/support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -785,14 +785,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)


Expand Down
24 changes: 22 additions & 2 deletions Lib/test/test_gdb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
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=['bt', '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
Loading

0 comments on commit bbce8bd

Please sign in to comment.