Repository navigation
Fixes for lineinfo emission #7196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
3631bdb
f6bef9d
5c3d7ae
2eacea6
8f1c198
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -22,6 +22,7 @@ | |||||
| from contextlib import contextmanager | ||||||
| import uuid | ||||||
| import importlib | ||||||
| import types as pytypes | ||||||
|
|
||||||
| import numpy as np | ||||||
|
|
||||||
|
|
@@ -113,6 +114,13 @@ | |||||
|
|
||||||
| needs_blas = unittest.skipUnless(has_blas, "BLAS needs SciPy 1.0+") | ||||||
|
|
||||||
| # Decorate a test with @needs_subprocess to ensure it doesn't run unless the | ||||||
| # `SUBPROC_TEST` environment variable is set. Use this in conjunction with: | ||||||
| # TestCase::subprocess_test_runner which will execute a given test in subprocess | ||||||
| # with this environment variable set. | ||||||
| _exec_cond = os.environ.get('SUBPROC_TEST', None) == '1' | ||||||
| needs_subprocess = unittest.skipUnless(_exec_cond, "needs subprocess harness") | ||||||
|
|
||||||
|
|
||||||
| def ignore_internal_warnings(): | ||||||
| """Use in testing within a ` warnings.catch_warnings` block to filter out | ||||||
|
|
@@ -502,6 +510,46 @@ def run_nullary_func(self, pyfunc, flags): | |||||
| self.assertPreciseEqual(got, expected) | ||||||
| return got, expected | ||||||
|
|
||||||
| def subprocess_test_runner(self, test_module, test_class=None, | ||||||
| test_name=None, envvars=None, timeout=60): | ||||||
| """ | ||||||
| Runs named unit test(s) as specified in the arguments as: | ||||||
| test_module.test_class.test_name. test_module must always be supplied | ||||||
| and if no further refinement is made with test_class and test_name then | ||||||
| all tests in the module will be run. The tests will be run in a | ||||||
| subprocess with environment variables specified in `envvars`. | ||||||
| If given, envvars must be a map of form: | ||||||
| environment variable name (str) -> value (str) | ||||||
| It is most convenient to use this method in conjunction with | ||||||
| @needs_subprocess as the decorator will cause the decorated test to be | ||||||
| skipped unless the `SUBPROC_TEST` environment variable is set | ||||||
| (this special environment variable is set by this method such that the | ||||||
| specified test(s) will not be skipped in the subprocess). | ||||||
|
|
||||||
|
|
||||||
| Following execution in the subprocess this method will check the test(s) | ||||||
| executed without error. The timeout kwarg can be used to allow more time | ||||||
| for longer running tests, it defaults to 60 seconds. | ||||||
| """ | ||||||
| themod = self.__module__ | ||||||
| thecls = type(self).__name__ | ||||||
| parts = (test_module, test_class, test_name) | ||||||
| fully_qualified_test = '.'.join(x for x in parts if x is not None) | ||||||
| cmd = [sys.executable, '-m', 'numba.runtests', fully_qualified_test] | ||||||
| env_copy = os.environ.copy() | ||||||
| env_copy['SUBPROC_TEST'] = '1' | ||||||
| envvars = pytypes.MappingProxyType({} if envvars is None else envvars) | ||||||
| env_copy.update(envvars) | ||||||
| status = subprocess.run(cmd, stdout=subprocess.PIPE, | ||||||
| stderr=subprocess.PIPE, timeout=timeout, | ||||||
| env=env_copy, universal_newlines=True) | ||||||
| streams = (f'\ncaptured stdout: {status.stdout}\n' | ||||||
| f'captured stderr: {status.stderr}') | ||||||
| self.assertEqual(status.returncode, 0, streams) | ||||||
| self.assertIn('OK', status.stderr) | ||||||
| self.assertTrue('FAIL' not in status.stderr) | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| self.assertTrue('ERROR' not in status.stderr) | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
|
|
||||||
| class SerialMixin(object): | ||||||
| """Mixin to mark test for serial execution. | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,10 +1,7 @@ | ||||||
| import inspect | ||||||
| import os | ||||||
| import re | ||||||
| import subprocess | ||||||
| import sys | ||||||
|
|
||||||
| from numba.tests.support import TestCase, override_config | ||||||
| from numba.tests.support import TestCase, override_config, needs_subprocess | ||||||
| from numba import jit, njit | ||||||
| from numba.core import types | ||||||
| import unittest | ||||||
|
|
@@ -55,15 +52,12 @@ def bar(x): | |||||
| self._check(bar, sig=(types.int32,), expect=False) | ||||||
|
|
||||||
|
|
||||||
| # some tests need clean environments to hide optimisation default states | ||||||
| _exec_cond = os.environ.get('SUBPROC_TEST', None) == '1' | ||||||
| needs_subprocess = unittest.skipUnless(_exec_cond, "needs subprocess harness") | ||||||
|
|
||||||
|
|
||||||
| class TestDebugInfoEmission(TestCase): | ||||||
| """ Tests that debug info is emitted correctly. | ||||||
| """ | ||||||
|
|
||||||
| _NUMBA_OPT_0_ENV = {'NUMBA_OPT': '0'} | ||||||
|
|
||||||
| def _get_llvmir(self, fn, sig): | ||||||
| with override_config('OPT', 0): | ||||||
| fn.compile(sig) | ||||||
|
|
@@ -78,6 +72,14 @@ def _get_metadata(self, fn, sig): | |||||
| metadata.append(line) | ||||||
| return metadata | ||||||
|
|
||||||
| def _subprocess_test_runner(self, test_name): | ||||||
| themod = self.__module__ | ||||||
| thecls = type(self).__name__ | ||||||
| self.subprocess_test_runner(test_module=themod, | ||||||
| test_class=thecls, | ||||||
| test_name=test_name, | ||||||
| envvars=self._NUMBA_OPT_0_ENV) | ||||||
|
|
||||||
| def test_DW_LANG(self): | ||||||
|
|
||||||
| @njit(debug=True) | ||||||
|
|
@@ -263,24 +265,121 @@ def test_DILocation_entry_blk(self): | |||||
| # as jitting literally anything at any point in the lifetime of the | ||||||
| # process ends up with a codegen at opt 3. This is not amenable to this | ||||||
| # test! | ||||||
| themod = self.__module__ | ||||||
| thecls = type(self).__name__ | ||||||
| injected_method = f'{themod}.{thecls}.test_DILocation_entry_blk_impl' | ||||||
| cmd = [sys.executable, '-m', 'numba.runtests', injected_method] | ||||||
| env_copy = os.environ.copy() | ||||||
| env_copy['SUBPROC_TEST'] = '1' | ||||||
| # This test relies on the CFG not being simplified as it checks the jump | ||||||
| # from the entry block to the first basic block. Force OPT as 0, if set | ||||||
| # via the env var the targetmachine and various pass managers all end up | ||||||
| # at OPT 0 and the IR is minimally transformed prior to lowering to ELF. | ||||||
| env_copy['NUMBA_OPT'] = '0' | ||||||
| status = subprocess.run(cmd, stdout=subprocess.PIPE, | ||||||
| stderr=subprocess.PIPE, timeout=60, | ||||||
| env=env_copy, universal_newlines=True) | ||||||
| self.assertEqual(status.returncode, 0) | ||||||
| self.assertIn('OK', status.stderr) | ||||||
| self.assertTrue('FAIL' not in status.stderr) | ||||||
| self.assertTrue('ERROR' not in status.stderr) | ||||||
| self._subprocess_test_runner('test_DILocation_entry_blk_impl') | ||||||
|
|
||||||
| @needs_subprocess | ||||||
| def test_DILocation_decref_impl(self): | ||||||
| """ This tests that decref's generated from `ir.Del`s as variables go | ||||||
| out of scope do not have debuginfo associated with them (the location of | ||||||
| `ir.Del` is an implementation detail). | ||||||
| """ | ||||||
|
|
||||||
| @njit(debug=True) | ||||||
| def sink(*x): | ||||||
| pass | ||||||
|
|
||||||
| # This function has many decrefs! | ||||||
| @njit(debug=True) | ||||||
| def foo(a): | ||||||
| x = (a, a) | ||||||
| if a[0] == 0: | ||||||
| sink(x) | ||||||
| return 12 | ||||||
| z = x[0][0] | ||||||
| return z | ||||||
|
|
||||||
| sig = (types.float64[::1],) | ||||||
| full_ir = self._get_llvmir(foo, sig=sig) | ||||||
|
|
||||||
| # make sure decref lines end with `meminfo.<number>)`. | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| count = 0 | ||||||
| for line in full_ir.splitlines(): | ||||||
| line_stripped = line.strip() | ||||||
| if line_stripped.startswith('call void @NRT_decref'): | ||||||
| self.assertRegex(line, r'.*meminfo\.[0-9]+\)$') | ||||||
| count += 1 | ||||||
| self.assertTrue(count > 0) # make sure there were some decrefs! | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| def test_DILocation_decref(self): | ||||||
| # Test runner for test_DILocation_decref_impl, needs a subprocess | ||||||
| # with opt=0 to preserve decrefs. | ||||||
| self._subprocess_test_runner('test_DILocation_decref_impl') | ||||||
|
|
||||||
| def test_DILocation_undefined(self): | ||||||
| """ Tests that DILocation information for undefined vars is associated | ||||||
| with the line of the function definition (so it ends up in the prologue) | ||||||
| """ | ||||||
| @njit(debug=True) | ||||||
| def foo(n): | ||||||
| if n: | ||||||
| if n > 0: | ||||||
| c = 0 | ||||||
| return c | ||||||
| else: | ||||||
| # variable c is not defined in this branch | ||||||
| c += 1 | ||||||
| return c | ||||||
|
|
||||||
| sig = (types.intp,) | ||||||
| metadata = self._get_metadata(foo, sig=sig) | ||||||
| pysrc, pysrc_line_start = inspect.getsourcelines(foo) | ||||||
| # Looks for versions of variable "c" and captures the line number | ||||||
| expr = r'.*!DILocalVariable\(name: "c\$?[0-9]?",.*line: ([0-9]+),.*' | ||||||
| matcher = re.compile(expr) | ||||||
| associated_lines = set() | ||||||
| for md in metadata: | ||||||
| match = matcher.match(md) | ||||||
| if match: | ||||||
| groups = match.groups() | ||||||
| self.assertEqual(len(groups), 1) | ||||||
| associated_lines.add(int(groups[0])) | ||||||
| self.assertEqual(len(associated_lines), 3) # 3 versions of 'c' | ||||||
| self.assertIn(pysrc_line_start, associated_lines) | ||||||
|
|
||||||
| def test_DILocation_versioned_variables(self): | ||||||
| """ Tests that DILocation information for versions of variables matches | ||||||
| up to their definition site.""" | ||||||
| # Note: there's still something wrong in the DI/SSA naming, the ret c is | ||||||
| # associated with the logically first definition. | ||||||
|
|
||||||
| @njit(debug=True) | ||||||
| def foo(n): | ||||||
| if n: | ||||||
| c = 5 | ||||||
| else: | ||||||
| c = 1 | ||||||
| return c | ||||||
|
|
||||||
| sig = (types.intp,) | ||||||
| metadata = self._get_metadata(foo, sig=sig) | ||||||
| pysrc, pysrc_line_start = inspect.getsourcelines(foo) | ||||||
|
|
||||||
| # Looks for SSA versioned names i.e. <basename>$<version id> of the | ||||||
| # variable 'c' and captures the line | ||||||
| expr = r'.*!DILocalVariable\(name: "c\$[0-9]?",.*line: ([0-9]+),.*' | ||||||
| matcher = re.compile(expr) | ||||||
| associated_lines = set() | ||||||
| for md in metadata: | ||||||
| match = matcher.match(md) | ||||||
| if match: | ||||||
| groups = match.groups() | ||||||
| self.assertEqual(len(groups), 1) | ||||||
| associated_lines.add(int(groups[0])) | ||||||
| self.assertEqual(len(associated_lines), 2) # 2 SSA versioned names 'c' | ||||||
|
|
||||||
| # Now find the `c = ` lines in the python source | ||||||
| py_lines = set() | ||||||
| for ix, pyln in enumerate(pysrc): | ||||||
| if 'c = ' in pyln: | ||||||
| py_lines.add(ix + pysrc_line_start) | ||||||
| self.assertEqual(len(py_lines), 2) # 2 assignments to c | ||||||
|
|
||||||
| # check that the DILocation from the DI for `c` matches the python src | ||||||
| self.assertEqual(associated_lines, py_lines) | ||||||
|
|
||||||
|
|
||||||
| if __name__ == '__main__': | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's still a copy of this in test_parfors.py
numba/numba/tests/test_parfors.py
Lines 56 to 57 in 2eacea6