Repository: numba/numba

Issue:
Update GDB configuration to deal with both gdb binary name and explicit path.
As noted here: https://github.com/numba/numba/pull/7617#discussion_r792783284 the gdb configuration needs an update to deal with path vs. binary name that needs to be searched for.


Discussion:

Resolving patch:
diff --git a/numba/core/config.py b/numba/core/config.py
--- a/numba/core/config.py
+++ b/numba/core/config.py
@@ -2,6 +2,7 @@
 import sys
 import os
 import re
+import shutil
 import warnings
 
 # YAML needed to use file based Numba config
@@ -465,7 +466,11 @@ def num_threads_default():
                                              int, 0)
 
         # gdb binary location
-        GDB_BINARY = _readenv("NUMBA_GDB_BINARY", str, '/usr/bin/gdb')
+        def which_gdb(path_or_bin):
+            gdb = shutil.which(path_or_bin)
+            return gdb if gdb is not None else path_or_bin
+
+        GDB_BINARY = _readenv("NUMBA_GDB_BINARY", which_gdb, 'gdb')
 
         # CUDA Memory management
         CUDA_MEMORY_MANAGER = _readenv("NUMBA_CUDA_MEMORY_MANAGER", str,
diff --git a/numba/misc/numba_gdbinfo.py b/numba/misc/numba_gdbinfo.py
--- a/numba/misc/numba_gdbinfo.py
+++ b/numba/misc/numba_gdbinfo.py
@@ -2,7 +2,6 @@
 from collections import namedtuple
 import os
 import re
-import shutil
 import subprocess
 from textwrap import dedent
 from numba import config
@@ -17,7 +16,7 @@ class _GDBTestWrapper():
     has support for (Python and NumPy)."""
 
     def __init__(self,):
-        gdb_binary = shutil.which(config.GDB_BINARY)
+        gdb_binary = config.GDB_BINARY
         if gdb_binary is None:
             msg = ("No valid binary could be found for gdb named: "
                    f"{config.GDB_BINARY}")
@@ -66,42 +65,52 @@ def check_numpy_version(self):
 def collect_gdbinfo():
     """Prints information to stdout about the gdb setup that Numba has found"""
 
-    # Check gdb exists
-    gdb_wrapper = _GDBTestWrapper()
-
     # State flags:
+    gdb_state = None
     gdb_has_python = False
     gdb_has_numpy = False
     gdb_python_version = 'No Python support'
     gdb_python_numpy_version = "No NumPy support"
 
-    # Check gdb works
-    status = gdb_wrapper.check_launch()
-    if not gdb_wrapper.success(status):
-        msg = (f"gdb at '{gdb_wrapper.gdb_binary}' does not appear to work."
-               f"\nstdout: {status.stdout}\nstderr: {status.stderr}")
-        raise ValueError(msg)
-
-    # Got this far, so gdb works, start checking what it supports
-    status = gdb_wrapper.check_python()
-    if gdb_wrapper.success(status):
-        version_match = re.match(r'\((\d+),\s+(\d+)\)', status.stdout.strip())
-        if version_match is not None:
-            pymajor, pyminor = version_match.groups()
-            gdb_python_version = f"{pymajor}.{pyminor}"
-            gdb_has_python = True
-
-            status = gdb_wrapper.check_numpy()
-            if gdb_wrapper.success(status):
-                if "Traceback" not in status.stderr.strip():
-                    if status.stdout.strip() == 'True':
-                        gdb_has_numpy = True
-                        gdb_python_numpy_version = "Unknown"
-                        # NumPy is present find the version
-                        status = gdb_wrapper.check_numpy_version()
-                        if gdb_wrapper.success(status):
-                            if "Traceback" not in status.stderr.strip():
-                                gdb_python_numpy_version = status.stdout.strip()
+    # There are so many ways for gdb to not be working as expected. Surround
+    # the "is it working" tests with try/except and if there's an exception
+    # store it for processing later.
+    try:
+        # Check gdb exists
+        gdb_wrapper = _GDBTestWrapper()
+
+        # Check gdb works
+        status = gdb_wrapper.check_launch()
+        if not gdb_wrapper.success(status):
+            msg = (f"gdb at '{gdb_wrapper.gdb_binary}' does not appear to work."
+                   f"\nstdout: {status.stdout}\nstderr: {status.stderr}")
+            raise ValueError(msg)
+        gdb_state = gdb_wrapper.gdb_binary
+    except Exception as e:
+        gdb_state = f"Testing gdb binary failed. Reported Error: {e}"
+    else:
+        # Got this far, so gdb works, start checking what it supports
+        status = gdb_wrapper.check_python()
+        if gdb_wrapper.success(status):
+            version_match = re.match(r'\((\d+),\s+(\d+)\)',
+                                     status.stdout.strip())
+            if version_match is not None:
+                pymajor, pyminor = version_match.groups()
+                gdb_python_version = f"{pymajor}.{pyminor}"
+                gdb_has_python = True
+
+                status = gdb_wrapper.check_numpy()
+                if gdb_wrapper.success(status):
+                    if "Traceback" not in status.stderr.strip():
+                        if status.stdout.strip() == 'True':
+                            gdb_has_numpy = True
+                            gdb_python_numpy_version = "Unknown"
+                            # NumPy is present find the version
+                            status = gdb_wrapper.check_numpy_version()
+                            if gdb_wrapper.success(status):
+                                if "Traceback" not in status.stderr.strip():
+                                    gdb_python_numpy_version = \
+                                        status.stdout.strip()
 
     # Work out what level of print-extension support is present in this gdb
     if gdb_has_python:
@@ -117,7 +126,7 @@ def collect_gdbinfo():
     print_ext_path = os.path.join(os.path.dirname(__file__), print_ext_file)
 
     # return!
-    return _gdb_info(gdb_wrapper.gdb_binary, print_ext_path, gdb_python_version,
+    return _gdb_info(gdb_state, print_ext_path, gdb_python_version,
                      gdb_python_numpy_version, print_ext_supported)
 
 


Test patch:
diff --git a/numba/tests/gdb/test_pretty_print.py b/numba/tests/gdb/test_pretty_print.py
--- a/numba/tests/gdb/test_pretty_print.py
+++ b/numba/tests/gdb/test_pretty_print.py
@@ -1,13 +1,14 @@
 # NOTE: This test is sensitive to line numbers as it checks breakpoints
 from numba import njit
 import numpy as np
-from numba.tests.gdb_support import GdbMIDriver
+from numba.tests.gdb_support import GdbMIDriver, needs_gdb_py3
 from numba.tests.support import TestCase, needs_subprocess
-import os
+from numba.misc.numba_gdbinfo import collect_gdbinfo
 import unittest
 import re
 
 
+@needs_gdb_py3
 @needs_subprocess
 class Test(TestCase):
 
@@ -29,9 +30,9 @@ def foo():
 
         foo()
 
-        extension = os.path.join('numba', 'misc', 'gdb_print_extension.py')
+        extension = collect_gdbinfo().extension_loc
         driver = GdbMIDriver(__file__, init_cmds=['-x', extension], debug=False)
-        driver.set_breakpoint(line=28)
+        driver.set_breakpoint(line=29)
         driver.run()
         driver.check_hit_breakpoint(1)
 
diff --git a/numba/tests/gdb_support.py b/numba/tests/gdb_support.py
--- a/numba/tests/gdb_support.py
+++ b/numba/tests/gdb_support.py
@@ -5,17 +5,25 @@
 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`.
+    _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
@@ -52,7 +60,7 @@ def __init__(self, file_name, debug=False, timeout=120, init_cmds=None):
     def _drive(self):
         """This function sets up the caputured gdb instance"""
         assert os.path.isfile(self._file_name)
-        cmd = [self._gdb_binary, '--interpreter', 'mi',]
+        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]
@@ -169,7 +177,7 @@ def interpreter_exec(self, interpreter=None, command=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 `,`
+        self._run_command(cmd, expect=r'\^(done|error).*\r\n')  # NOTE no `,`
 
     def _list_features_raw(self):
         cmd = '-list-features'
diff --git a/numba/tests/test_cli.py b/numba/tests/test_cli.py
--- a/numba/tests/test_cli.py
+++ b/numba/tests/test_cli.py
@@ -10,7 +10,7 @@
 from unittest import mock
 
 import unittest
-from numba.tests.support import TestCase
+from numba.tests.support import TestCase, linux_only
 import numba.misc.numba_sysinfo as nsi
 from numba.tests.gdb_support import needs_gdb
 from numba.misc.numba_gdbinfo import collect_gdbinfo
@@ -184,10 +184,10 @@ def mock_fn(self):
             return CompletedProcess('INVALID_BINARY', 1)
 
         with mock.patch.object(_GDBTestWrapper, 'check_launch', mock_fn):
-            with self.assertRaises(ValueError) as raises:
-                collect_gdbinfo()
-            self.assertRegex(str(raises.exception),
-                             'gdb at.*does not appear to work.')
+            info = collect_gdbinfo()
+            self.assertIn("Testing gdb binary failed.", info.binary_loc)
+            self.assertIn("gdb at 'PATH_TO_GDB' does not appear to work",
+                          info.binary_loc)
 
     def test_no_python(self):
         def mock_fn(self):
@@ -235,5 +235,47 @@ def mock_fn(self):
             self.assertEqual(collected.np_ver, 'Unknown')
 
 
+@linux_only
+class TestGDBCLIInfoBrokenGdbs(TestCase):
+    # In these tests, it's necessary to actually run the entry point in an env
+    # with the env var set as it's in part testing the processing of the
+    # handling of consumption of variables by numba.core.config.
+
+    def test_cannot_find_gdb_from_name(self):
+        # Tests that supplying an invalid gdb binary name is handled ok
+        env = os.environ.copy()
+        env['NUMBA_GDB_BINARY'] = 'THIS_IS_NOT_A_VALID_GDB_BINARY_NAME'
+        cmdline = [sys.executable, "-m", "numba", "-g"]
+        stdout, stderr = run_cmd(cmdline, env=env)
+        self.assertIn("Testing gdb binary failed", stdout)
+        self.assertIn("No such file or directory", stdout)
+        self.assertIn("'THIS_IS_NOT_A_VALID_GDB_BINARY_NAME'", stdout)
+
+    def test_cannot_find_gdb_from_path(self):
+        # Tests that an invalid path is handled ok
+        env = os.environ.copy()
+        with TemporaryDirectory() as d:
+            # This wont exist as a path...
+            path = os.path.join(d, 'CANNOT_EXIST')
+            env['NUMBA_GDB_BINARY'] = path
+            cmdline = [sys.executable, "-m", "numba", "-g"]
+            stdout, stderr = run_cmd(cmdline, env=env)
+            self.assertIn("Testing gdb binary failed", stdout)
+            self.assertIn("No such file or directory", stdout)
+            self.assertIn(path, stdout)
+
+    def test_nonsense_gdb_binary(self):
+        # Tests that a nonsense binary specified as gdb it picked up ok
+        env = os.environ.copy()
+        env['NUMBA_GDB_BINARY'] = 'python' # 'python' isn't gdb!
+        cmdline = [sys.executable, "-m", "numba", "-g"]
+        stdout, stderr = run_cmd(cmdline, env=env)
+        self.assertIn("Testing gdb binary failed", stdout)
+        # NOTE: should 'python' ever add support for the same flags as the gdb
+        # commands used in the infomation gathering code in `numba_gdbinfo`
+        # this test will fail, it's reasonably unlikely.
+        self.assertIn("Unknown option", stdout)
+
+
 if __name__ == '__main__':
     unittest.main()