""" Tests gdb bindings """ import os import platform import re import subprocess import sys import threading from itertools import permutations from numba import njit, gdb, gdb_init, gdb_breakpoint, prange from numba.core import errors from numba import jit from numba.tests.support import (TestCase, captured_stdout, tag, skip_parfors_unsupported) from numba.tests.gdb_support import needs_gdb import unittest _platform = sys.platform _unix_like = (_platform.startswith('linux') or _platform.startswith('darwin') or ('bsd' in _platform)) unix_only = unittest.skipUnless(_unix_like, "unix-like OS is required") not_unix = unittest.skipIf(_unix_like, "non unix-like OS is required") _arch_name = platform.machine() _is_arm = _arch_name in {'aarch64', 'armv7l'} not_arm = unittest.skipIf(_is_arm, "testing disabled on ARM") _gdb_cond = os.environ.get('GDB_TEST', None) == '1' needs_gdb_harness = unittest.skipUnless(_gdb_cond, "needs gdb harness") long_running = tag('long_running') _dbg_njit = njit(debug=True) _dbg_jit = jit(forceobj=True, debug=True) def impl_gdb_call(a): gdb('-ex', 'set confirm off', '-ex', 'c', '-ex', 'q') b = a + 1 c = a * 2.34 d = (a, b, c) print(a, b, c, d) def impl_gdb_call_w_bp(a): gdb_init('-ex', 'set confirm off', '-ex', 'c', '-ex', 'q') b = a + 1 c = a * 2.34 d = (a, b, c) gdb_breakpoint() print(a, b, c, d) def impl_gdb_split_init_and_break_w_parallel(a): gdb_init('-ex', 'set confirm off', '-ex', 'c', '-ex', 'q') a += 3 for i in prange(4): b = a + 1 c = a * 2.34 d = (a, b, c) gdb_breakpoint() print(a, b, c, d) @not_arm @unix_only class TestGdbBindImpls(TestCase): """ Contains unit test implementations for gdb binding testing. Test must be decorated with `@needs_gdb_harness` to prevent their running under normal test conditions, the test methods must also end with `_impl` to be considered for execution. The tests themselves are invoked by the `TestGdbBinding` test class through the parsing of this class for test methods and then running the discovered tests in a separate process. Test names not including the word `quick` will be tagged as @tag('long_running') """ @needs_gdb_harness def test_gdb_cmd_lang_cpython_quick_impl(self): with captured_stdout(): impl_gdb_call(10) @needs_gdb_harness def test_gdb_cmd_lang_nopython_quick_impl(self): with captured_stdout(): _dbg_njit(impl_gdb_call)(10) @needs_gdb_harness def test_gdb_cmd_lang_objmode_quick_impl(self): with captured_stdout(): _dbg_jit(impl_gdb_call)(10) @needs_gdb_harness def test_gdb_split_init_and_break_cpython_impl(self): with captured_stdout(): impl_gdb_call_w_bp(10) @needs_gdb_harness def test_gdb_split_init_and_break_nopython_impl(self): with captured_stdout(): _dbg_njit(impl_gdb_call_w_bp)(10) @needs_gdb_harness def test_gdb_split_init_and_break_objmode_impl(self): with captured_stdout(): _dbg_jit(impl_gdb_call_w_bp)(10) @skip_parfors_unsupported @needs_gdb_harness def test_gdb_split_init_and_break_w_parallel_cpython_impl(self): with captured_stdout(): impl_gdb_split_init_and_break_w_parallel(10) @skip_parfors_unsupported @needs_gdb_harness def test_gdb_split_init_and_break_w_parallel_nopython_impl(self): with captured_stdout(): _dbg_njit(impl_gdb_split_init_and_break_w_parallel)(10) @skip_parfors_unsupported @needs_gdb_harness def test_gdb_split_init_and_break_w_parallel_objmode_impl(self): with captured_stdout(): _dbg_jit(impl_gdb_split_init_and_break_w_parallel)(10) @not_arm @unix_only @needs_gdb class TestGdbBinding(TestCase): """ This test class is used to generate tests which will run the test cases defined in TestGdbBindImpls in isolated subprocesses, this is for safety in case something goes awry. """ # test mutates env _numba_parallel_test_ = False _DEBUG = True def run_cmd(self, cmdline, env, kill_is_ok=False): popen = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, shell=True) # finish in 20s or kill it, there's no work being done def kill(): popen.stdout.flush() popen.stderr.flush() popen.kill() timeout = threading.Timer(20., kill) try: timeout.start() out, err = popen.communicate() retcode = popen.returncode if retcode != 0: raise AssertionError( "process failed with code %s: " "stderr follows\n%s\n" "stdout :%s" % (retcode, err.decode(), out.decode())) return out.decode(), err.decode() finally: timeout.cancel() return None, None def run_test_in_separate_process(self, test, **kwargs): env_copy = os.environ.copy() env_copy['NUMBA_OPT'] = '1' # Set GDB_TEST to permit the execution of tests decorated with # @needs_gdb_harness env_copy['GDB_TEST'] = '1' cmdline = [sys.executable, "-m", "numba.runtests", test] return self.run_cmd(' '.join(cmdline), env_copy, **kwargs) @classmethod def _inject(cls, name): themod = TestGdbBindImpls.__module__ thecls = TestGdbBindImpls.__name__ # strip impl assert name.endswith('_impl') methname = name.replace('_impl', '') injected_method = '%s.%s.%s' % (themod, thecls, name) def test_template(self): o, e = self.run_test_in_separate_process(injected_method) dbgmsg = f'\nSTDOUT={o}\nSTDERR={e}\n' # If the test was skipped in the subprocess, then mark this as a # skipped test. m = re.search(r"\.\.\. skipped '(.*?)'", e) if m is not None: self.skipTest(m.group(1)) self.assertIn('GNU gdb', o, msg=dbgmsg) self.assertIn('OK', e, msg=dbgmsg) self.assertNotIn('FAIL', e, msg=dbgmsg) self.assertNotIn('ERROR', e, msg=dbgmsg) if 'quick' in name: setattr(cls, methname, test_template) else: setattr(cls, methname, long_running(test_template)) @classmethod def generate(cls): for name in dir(TestGdbBindImpls): if name.startswith('test_gdb'): cls._inject(name) TestGdbBinding.generate() @not_arm @unix_only @needs_gdb class TestGdbMisc(TestCase): @long_running def test_call_gdb_twice(self): def gen(f1, f2): @njit def impl(): a = 1 f1() b = 2 f2() return a + b return impl msg_head = "Calling either numba.gdb() or numba.gdb_init() more than" def check(func): with self.assertRaises(errors.UnsupportedError) as raises: func() self.assertIn(msg_head, str(raises.exception)) for g1, g2 in permutations([gdb, gdb_init]): func = gen(g1, g2) check(func) @njit def use_globals(): a = 1 gdb() b = 2 gdb_init() return a + b check(use_globals) @not_unix class TestGdbExceptions(TestCase): def test_call_gdb(self): def nop_compiler(x): return x for compiler in [nop_compiler, jit(forceobj=True), njit]: for meth in [gdb, gdb_init]: def python_func(): meth() with self.assertRaises(errors.TypingError) as raises: compiler(python_func)() msg = "gdb support is only available on unix-like systems" self.assertIn(msg, str(raises.exception)) if __name__ == '__main__': unittest.main()