198 lines
7.3 KiB
Python
198 lines
7.3 KiB
Python
"""Helpers for running gdb related testing"""
|
|
import os
|
|
import re
|
|
import sys
|
|
import unittest
|
|
from numba.core import config
|
|
from numba.misc.gdb_hook import _confirm_gdb
|
|
from numba.misc.numba_gdbinfo import collect_gdbinfo
|
|
|
|
# check if gdb is present and working
|
|
try:
|
|
_confirm_gdb(need_ptrace_attach=False) # The driver launches as `gdb EXE`.
|
|
_HAVE_GDB = True
|
|
_gdb_info = collect_gdbinfo()
|
|
_GDB_HAS_PY3 = _gdb_info.py_ver.startswith('3')
|
|
except Exception:
|
|
_HAVE_GDB = False
|
|
_GDB_HAS_PY3 = False
|
|
|
|
_msg = "functioning gdb with correct ptrace permissions is required"
|
|
needs_gdb = unittest.skipUnless(_HAVE_GDB, _msg)
|
|
|
|
_msg = "gdb with python 3 support needed"
|
|
needs_gdb_py3 = unittest.skipUnless(_GDB_HAS_PY3, _msg)
|
|
|
|
|
|
try:
|
|
import pexpect
|
|
_HAVE_PEXPECT = True
|
|
except ImportError:
|
|
_HAVE_PEXPECT = False
|
|
|
|
|
|
_msg = "pexpect module needed for test"
|
|
skip_unless_pexpect = unittest.skipUnless(_HAVE_PEXPECT, _msg)
|
|
|
|
|
|
class GdbMIDriver(object):
|
|
"""
|
|
Driver class for the GDB machine interface:
|
|
https://sourceware.org/gdb/onlinedocs/gdb/GDB_002fMI.html
|
|
"""
|
|
def __init__(self, file_name, debug=False, timeout=120, init_cmds=None):
|
|
if not _HAVE_PEXPECT:
|
|
msg = ("This driver requires the pexpect module. This can be "
|
|
"obtained via:\n\n$ conda install pexpect")
|
|
raise RuntimeError(msg)
|
|
if not _HAVE_GDB:
|
|
msg = ("This driver requires a gdb binary. This can be "
|
|
"obtained via the system package manager.")
|
|
raise RuntimeError(msg)
|
|
self._gdb_binary = config.GDB_BINARY
|
|
self._python = sys.executable
|
|
self._debug = debug
|
|
self._file_name = file_name
|
|
self._timeout = timeout
|
|
self._init_cmds = init_cmds
|
|
self._drive()
|
|
|
|
def _drive(self):
|
|
"""This function sets up the caputured gdb instance"""
|
|
assert os.path.isfile(self._file_name)
|
|
cmd = [self._gdb_binary, '--interpreter', 'mi']
|
|
if self._init_cmds is not None:
|
|
cmd += list(self._init_cmds)
|
|
cmd += ['--args', self._python, self._file_name]
|
|
self._captured = pexpect.spawn(' '.join(cmd))
|
|
if self._debug:
|
|
self._captured.logfile = sys.stdout.buffer
|
|
|
|
def supports_python(self):
|
|
"""Returns True if the underlying gdb implementation has python support
|
|
False otherwise"""
|
|
return "python" in self.list_features()
|
|
|
|
def supports_numpy(self):
|
|
"""Returns True if the underlying gdb implementation has NumPy support
|
|
(and by extension Python support) False otherwise"""
|
|
if not self.supports_python():
|
|
return False
|
|
# Some gdb's have python 2!
|
|
cmd = ('python from __future__ import print_function;'
|
|
'import numpy; print(numpy)')
|
|
self.interpreter_exec('console', cmd)
|
|
return "module \'numpy\' from" in self._captured.before.decode()
|
|
|
|
def _captured_expect(self, expect):
|
|
try:
|
|
self._captured.expect(expect, timeout=self._timeout)
|
|
except pexpect.exceptions.TIMEOUT as e:
|
|
msg = f"Expected value did not arrive: {expect}."
|
|
raise ValueError(msg) from e
|
|
|
|
def assert_output(self, expected):
|
|
"""Asserts that the current output string contains the expected."""
|
|
output = self._captured.after
|
|
decoded = output.decode('utf-8')
|
|
assert expected in decoded, f'decoded={decoded}\nexpected={expected})'
|
|
|
|
def assert_regex_output(self, expected):
|
|
"""Asserts that the current output string contains the expected
|
|
regex."""
|
|
output = self._captured.after
|
|
decoded = output.decode('utf-8')
|
|
done_str = decoded.splitlines()[0]
|
|
found = re.match(expected, done_str)
|
|
assert found, f'decoded={decoded}\nexpected={expected})'
|
|
|
|
def _run_command(self, command, expect=''):
|
|
self._captured.sendline(command)
|
|
self._captured_expect(expect)
|
|
|
|
def run(self):
|
|
"""gdb command ~= 'run'"""
|
|
self._run_command('-exec-run', expect=r'\^running.*\r\n')
|
|
|
|
def cont(self):
|
|
"""gdb command ~= 'continue'"""
|
|
self._run_command('-exec-continue', expect=r'\^running.*\r\n')
|
|
|
|
def quit(self):
|
|
"""gdb command ~= 'quit'"""
|
|
self._run_command('-gdb-exit', expect=r'-gdb-exit')
|
|
self._captured.terminate()
|
|
|
|
def next(self):
|
|
"""gdb command ~= 'next'"""
|
|
self._run_command('-exec-next', expect=r'\*stopped,.*\r\n')
|
|
|
|
def step(self):
|
|
"""gdb command ~= 'step'"""
|
|
self._run_command('-exec-step', expect=r'\*stopped,.*\r\n')
|
|
|
|
def set_breakpoint(self, line=None, symbol=None, condition=None):
|
|
"""gdb command ~= 'break'"""
|
|
if line is not None and symbol is not None:
|
|
raise ValueError("Can only supply one of line or symbol")
|
|
bp = '-break-insert '
|
|
if condition is not None:
|
|
bp += f'-c "{condition}" '
|
|
if line is not None:
|
|
assert isinstance(line, int)
|
|
bp += f'-f {self._file_name}:{line} '
|
|
if symbol is not None:
|
|
assert isinstance(symbol, str)
|
|
bp += f'-f {symbol} '
|
|
self._run_command(bp, expect=r'\^done')
|
|
|
|
def check_hit_breakpoint(self, number=None, line=None):
|
|
"""Checks that a breakpoint has been hit"""
|
|
self._captured_expect(r'\*stopped,.*\r\n')
|
|
self.assert_output('*stopped,reason="breakpoint-hit",')
|
|
if number is not None:
|
|
assert isinstance(number, int)
|
|
self.assert_output(f'bkptno="{number}"')
|
|
if line is not None:
|
|
assert isinstance(line, int)
|
|
self.assert_output(f'line="{line}"')
|
|
|
|
def stack_list_arguments(self, print_values=1, low_frame=0, high_frame=0):
|
|
"""gdb command ~= 'info args'"""
|
|
for x in (print_values, low_frame, high_frame):
|
|
assert isinstance(x, int) and x in (0, 1, 2)
|
|
cmd = f'-stack-list-arguments {print_values} {low_frame} {high_frame}'
|
|
self._run_command(cmd, expect=r'\^done,.*\r\n')
|
|
|
|
def stack_list_variables(self, print_values=1):
|
|
"""gdb command ~= 'info locals'"""
|
|
assert isinstance(print_values, int) and print_values in (0, 1, 2)
|
|
cmd = f'-stack-list-variables {print_values}'
|
|
self._run_command(cmd, expect=r'\^done,.*\r\n')
|
|
|
|
def interpreter_exec(self, interpreter=None, command=None):
|
|
"""gdb command ~= 'interpreter-exec'"""
|
|
if interpreter is None:
|
|
raise ValueError("interpreter cannot be None")
|
|
if command is None:
|
|
raise ValueError("command cannot be None")
|
|
cmd = f'-interpreter-exec {interpreter} "{command}"'
|
|
self._run_command(cmd, expect=r'\^(done|error).*\r\n') # NOTE no `,`
|
|
|
|
def _list_features_raw(self):
|
|
cmd = '-list-features'
|
|
self._run_command(cmd, expect=r'\^done,.*\r\n')
|
|
|
|
def list_features(self):
|
|
"""No equivalent gdb command? Returns a list of supported gdb
|
|
features.
|
|
"""
|
|
self._list_features_raw()
|
|
output = self._captured.after
|
|
decoded = output.decode('utf-8')
|
|
m = re.match('.*features=\\[(.*)\\].*', decoded)
|
|
assert m is not None, "No match found for features string"
|
|
g = m.groups()
|
|
assert len(g) == 1, "Invalid number of match groups found"
|
|
return g[0].replace('"', '').split(',')
|