Bug 1526062 - Improve error reporting by the configure lint. r=nalexander

Currently, running the configure lint will raise errors with little
context, if any, which can make finding the cause of the errors tedious.

So instead of raising exceptions directly, we use a hack to make the
exception appear as if they had been thrown at the code location an
issue is reported for, which makes the test harness print more useful
context.

With more context, printed out, some exception messages can be made
lighter.

The added _raise_from method does more than is necessary here, but will
be fully used in a new check shortly.

Depends on D19111

Differential Revision: https://phabricator.services.mozilla.com/D19112

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Mike Hommey 2019-02-08 22:47:02 +00:00
Родитель 845718811d
Коммит 83b2adb600
2 изменённых файлов: 141 добавлений и 63 удалений

Просмотреть файл

@ -45,19 +45,82 @@ class LintSandbox(ConfigureSandbox):
for dep in self._depends.itervalues():
self._check_dependencies(dep)
def _raise_from(self, exception, obj, line=0):
'''
Raises the given exception as if it were emitted from the given
location.
The location is determined from the values of obj and line.
- `obj` can be a function or DependsFunction, in which case
`line` corresponds to the line within the function the exception
will be raised from (as an offset from the function's firstlineno).
- `obj` can be a stack frame, in which case `line` is ignored.
'''
def thrower(e):
raise e
if isinstance(obj, DependsFunction):
obj, _ = self.unwrap(obj._func)
if inspect.isfunction(obj):
funcname = obj.__name__
filename = obj.func_code.co_filename
firstline = obj.func_code.co_firstlineno
line += firstline
elif inspect.isframe(obj):
funcname = obj.f_code.co_name
filename = obj.f_code.co_filename
firstline = obj.f_code.co_firstlineno
line = obj.f_lineno
else:
# Don't know how to handle the given location, still raise the
# exception.
raise exception
# Create a new function from the above thrower that pretends
# the `def` line is on the first line of the function given as
# argument, and the `raise` line is on the line given as argument.
offset = line - firstline
# co_lnotab is a string where each pair of consecutive character is
# (chr(byte_increment), chr(line_increment)), mapping bytes in co_code
# to line numbers relative to co_firstlineno.
# If the offset we need to encode is larger than 255, we need to split it.
co_lnotab = (chr(0) + chr(255)) * (offset / 255) + chr(0) + chr(offset % 255)
code = thrower.func_code
code = types.CodeType(
code.co_argcount,
code.co_nlocals,
code.co_stacksize,
code.co_flags,
code.co_code,
code.co_consts,
code.co_names,
code.co_varnames,
filename,
funcname,
firstline,
co_lnotab
)
thrower = types.FunctionType(
code,
thrower.func_globals,
funcname,
thrower.func_defaults,
thrower.func_closure
)
thrower(exception)
def _check_dependencies(self, obj):
if isinstance(obj, CombinedDependsFunction) or obj in (self._always,
self._never):
return
func, glob = self.unwrap(obj._func)
loc = '%s:%d' % (func.func_code.co_filename,
func.func_code.co_firstlineno)
func_args = inspect.getargspec(func)
if func_args.keywords:
raise ConfigureError(
'%s: Keyword arguments are not allowed in @depends functions'
% loc
)
e = ConfigureError(
'Keyword arguments are not allowed in @depends functions')
self._raise_from(e, func)
all_args = list(func_args.args)
if func_args.varargs:
@ -77,10 +140,8 @@ class LintSandbox(ConfigureSandbox):
dep = dep.name
else:
dep = dep.option
raise ConfigureError(
'%s: The dependency on `%s` is unused.'
% (loc, dep)
)
e = ConfigureError('The dependency on `%s` is unused' % dep)
self._raise_from(e, func)
def _need_help_dependency(self, obj):
if isinstance(obj, (CombinedDependsFunction, TrivialDependsFunction)):
@ -119,13 +180,13 @@ class LintSandbox(ConfigureSandbox):
if with_help:
for arg in obj.dependencies:
if self._missing_help_dependency(arg):
raise ConfigureError(
"`%s` depends on '--help' and `%s`. "
"`%s` must depend on '--help'"
% (obj.name, arg.name, arg.name))
e = ConfigureError(
"Missing '--help' dependency because `%s` depends on "
"'--help' and `%s`" % (obj.name, arg.name))
self._raise_from(e, arg)
elif self._missing_help_dependency(obj):
raise ConfigureError("Missing @depends for `%s`: '--help'" %
obj.name)
e = ConfigureError("Missing '--help' dependency")
self._raise_from(e, obj)
return super(LintSandbox, self)._value_for_depends(obj)
def option_impl(self, *args, **kwargs):
@ -166,14 +227,18 @@ class LintSandbox(ConfigureSandbox):
}
for prefix, replacement in table[default].iteritems():
if name.startswith('--{}-'.format(prefix)):
raise ConfigureError(('{} should be used instead of '
'{} with default={}').format(
name.replace('--{}-'.format(prefix),
'--{}-'.format(replacement)),
name, default))
frame = inspect.currentframe()
while frame and frame.f_code.co_name != self.option_impl.__name__:
frame = frame.f_back
e = ConfigureError('{} should be used instead of '
'{} with default={}'.format(
name.replace('--{}-'.format(prefix),
'--{}-'.format(replacement)),
name, default))
self._raise_from(e, frame.f_back if frame else None)
def _check_help_for_option_with_func_default(self, option, *args, **kwargs):
name = args[0]
default = kwargs['default']
if not isinstance(default, SandboxDependsFunction):
@ -196,8 +261,13 @@ class LintSandbox(ConfigureSandbox):
else:
rule = '{With|Without}'
raise ConfigureError(('{} has a non-constant default. '
'Its help should contain "{}"').format(name, rule))
frame = inspect.currentframe()
while frame and frame.f_code.co_name != self.option_impl.__name__:
frame = frame.f_back
e = ConfigureError(
'`help` should contain "{}" because of non-constant default'
.format(rule))
self._raise_from(e, frame.f_back if frame else None)
def unwrap(self, func):
glob = func.func_globals

Просмотреть файл

@ -5,8 +5,11 @@
from __future__ import absolute_import, print_function, unicode_literals
from StringIO import StringIO
import contextlib
import os
import sys
import textwrap
import traceback
import unittest
from mozunit import (
@ -35,6 +38,16 @@ class TestLint(unittest.TestCase):
'moz.configure'): textwrap.dedent(source)
})
@contextlib.contextmanager
def assertRaisesFromLine(self, exc_type, line):
with self.assertRaises(exc_type) as e:
yield e
_, _, tb = sys.exc_info()
self.assertEquals(
traceback.extract_tb(tb)[-1][:2],
(mozpath.join(test_data_path, 'moz.configure'), line))
def test_configure_testcase(self):
# Lint python/mozbuild/mozbuild/test/configure/data/moz.configure
self.lint_test()
@ -53,7 +66,7 @@ class TestLint(unittest.TestCase):
'''):
self.lint_test()
with self.assertRaises(ConfigureError) as e:
with self.assertRaisesFromLine(ConfigureError, 7) as e:
with self.moz_configure('''
option('--foo', help='foo')
@depends('--foo')
@ -67,10 +80,9 @@ class TestLint(unittest.TestCase):
self.lint_test()
self.assertEquals(e.exception.message,
"%s:7: The dependency on `--help` is unused."
% mozpath.join(test_data_path, 'moz.configure'))
"The dependency on `--help` is unused")
with self.assertRaises(ConfigureError) as e:
with self.assertRaisesFromLine(ConfigureError, 3) as e:
with self.moz_configure('''
option('--foo', help='foo')
@depends('--foo')
@ -85,11 +97,11 @@ class TestLint(unittest.TestCase):
'''):
self.lint_test()
self.assertEquals(e.exception.message,
"`bar` depends on '--help' and `foo`. "
"`foo` must depend on '--help'")
self.assertEquals(
e.exception.message,
"Missing '--help' dependency because `bar` depends on '--help' and `foo`")
with self.assertRaises(ConfigureError) as e:
with self.assertRaisesFromLine(ConfigureError, 7) as e:
with self.moz_configure('''
@template
def tmpl():
@ -109,9 +121,9 @@ class TestLint(unittest.TestCase):
'''):
self.lint_test()
self.assertEquals(e.exception.message,
"`bar` depends on '--help' and `foo`. "
"`foo` must depend on '--help'")
self.assertEquals(
e.exception.message,
"Missing '--help' dependency because `bar` depends on '--help' and `foo`")
with self.moz_configure('''
option('--foo', help='foo')
@ -123,7 +135,7 @@ class TestLint(unittest.TestCase):
'''):
self.lint_test()
with self.assertRaises(ConfigureError) as e:
with self.assertRaisesFromLine(ConfigureError, 3) as e:
with self.moz_configure('''
option('--foo', help='foo')
@depends('--foo')
@ -136,9 +148,9 @@ class TestLint(unittest.TestCase):
self.lint_test()
self.assertEquals(e.exception.message,
"Missing @depends for `foo`: '--help'")
"Missing '--help' dependency")
with self.assertRaises(ConfigureError) as e:
with self.assertRaisesFromLine(ConfigureError, 3) as e:
with self.moz_configure('''
option('--foo', help='foo')
@depends('--foo')
@ -155,9 +167,9 @@ class TestLint(unittest.TestCase):
self.lint_test()
self.assertEquals(e.exception.message,
"Missing @depends for `foo`: '--help'")
"Missing '--help' dependency")
with self.assertRaises(ConfigureError) as e:
with self.assertRaisesFromLine(ConfigureError, 3) as e:
with self.moz_configure('''
option('--foo', help='foo')
@depends('--foo')
@ -170,9 +182,9 @@ class TestLint(unittest.TestCase):
self.lint_test()
self.assertEquals(e.exception.message,
"Missing @depends for `foo`: '--help'")
"Missing '--help' dependency")
# This would have failed with "Missing @depends for `foo`: '--help'"
# This would have failed with "Missing '--help' dependency"
# in the past, because of the reference to the builtin False.
with self.moz_configure('''
option('--foo', help='foo')
@ -186,7 +198,7 @@ class TestLint(unittest.TestCase):
# However, when something that is normally a builtin is overridden,
# we should still want the dependency on --help.
with self.assertRaises(ConfigureError) as e:
with self.assertRaisesFromLine(ConfigureError, 7) as e:
with self.moz_configure('''
@template
def tmpl():
@ -203,7 +215,7 @@ class TestLint(unittest.TestCase):
self.lint_test()
self.assertEquals(e.exception.message,
"Missing @depends for `foo`: '--help'")
"Missing '--help' dependency")
# There is a default restricted `os` module when there is no explicit
# @imports, and it's fine to use it without a dependency on --help.
@ -218,7 +230,7 @@ class TestLint(unittest.TestCase):
'''):
self.lint_test()
with self.assertRaises(ConfigureError) as e:
with self.assertRaisesFromLine(ConfigureError, 3) as e:
with self.moz_configure('''
option('--foo', help='foo')
@depends('--foo')
@ -230,10 +242,9 @@ class TestLint(unittest.TestCase):
self.lint_test()
self.assertEquals(e.exception.message,
"%s:3: The dependency on `--foo` is unused."
% mozpath.join(test_data_path, 'moz.configure'))
"The dependency on `--foo` is unused")
with self.assertRaises(ConfigureError) as e:
with self.assertRaisesFromLine(ConfigureError, 5) as e:
with self.moz_configure('''
@depends(when=True)
def bar():
@ -247,10 +258,9 @@ class TestLint(unittest.TestCase):
self.lint_test()
self.assertEquals(e.exception.message,
"%s:5: The dependency on `bar` is unused."
% mozpath.join(test_data_path, 'moz.configure'))
"The dependency on `bar` is unused")
with self.assertRaises(ConfigureError) as e:
with self.assertRaisesFromLine(ConfigureError, 2) as e:
with self.moz_configure('''
@depends(depends(when=True)(lambda: None))
def foo(value):
@ -261,10 +271,9 @@ class TestLint(unittest.TestCase):
self.lint_test()
self.assertEquals(e.exception.message,
"%s:2: The dependency on `<lambda>` is unused."
% mozpath.join(test_data_path, 'moz.configure'))
"The dependency on `<lambda>` is unused")
with self.assertRaises(ConfigureError) as e:
with self.assertRaisesFromLine(ConfigureError, 9) as e:
with self.moz_configure('''
@template
def tmpl():
@ -282,8 +291,7 @@ class TestLint(unittest.TestCase):
self.lint_test()
self.assertEquals(e.exception.message,
"%s:9: The dependency on `qux` is unused."
% mozpath.join(test_data_path, 'moz.configure'))
"The dependency on `qux` is unused")
def test_default_enable(self):
# --enable-* with default=True is not allowed.
@ -291,7 +299,7 @@ class TestLint(unittest.TestCase):
option('--enable-foo', default=False, help='foo')
'''):
self.lint_test()
with self.assertRaises(ConfigureError) as e:
with self.assertRaisesFromLine(ConfigureError, 2) as e:
with self.moz_configure('''
option('--enable-foo', default=True, help='foo')
'''):
@ -306,7 +314,7 @@ class TestLint(unittest.TestCase):
option('--disable-foo', default=True, help='foo')
'''):
self.lint_test()
with self.assertRaises(ConfigureError) as e:
with self.assertRaisesFromLine(ConfigureError, 2) as e:
with self.moz_configure('''
option('--disable-foo', default=False, help='foo')
'''):
@ -321,7 +329,7 @@ class TestLint(unittest.TestCase):
option('--with-foo', default=False, help='foo')
'''):
self.lint_test()
with self.assertRaises(ConfigureError) as e:
with self.assertRaisesFromLine(ConfigureError, 2) as e:
with self.moz_configure('''
option('--with-foo', default=True, help='foo')
'''):
@ -336,7 +344,7 @@ class TestLint(unittest.TestCase):
option('--without-foo', default=True, help='foo')
'''):
self.lint_test()
with self.assertRaises(ConfigureError) as e:
with self.assertRaisesFromLine(ConfigureError, 2) as e:
with self.moz_configure('''
option('--without-foo', default=False, help='foo')
'''):
@ -354,7 +362,7 @@ class TestLint(unittest.TestCase):
help='{Enable|Disable} bar')
'''):
self.lint_test()
with self.assertRaises(ConfigureError) as e:
with self.assertRaisesFromLine(ConfigureError, 4) as e:
with self.moz_configure('''
option(env='FOO', help='foo')
option('--enable-bar', default=depends('FOO')(lambda x: bool(x)),
@ -362,8 +370,8 @@ class TestLint(unittest.TestCase):
'''):
self.lint_test()
self.assertEquals(e.exception.message,
'--enable-bar has a non-constant default. '
'Its help should contain "{Enable|Disable}"')
'`help` should contain "{Enable|Disable}" because of '
'non-constant default')
if __name__ == '__main__':
main()