Bug 1254374 - Add various failure tests to test_configure.py. r?nalexander
At the same time, improve some of the failures handling paths.
--- a/python/mozbuild/mozbuild/configure/__init__.py
+++ b/python/mozbuild/mozbuild/configure/__init__.py
@@ -36,18 +36,18 @@ import mozpack.path as mozpath
class ConfigureError(Exception):
pass
class DependsFunction(object):
'''Sandbox-visible representation of @depends functions.'''
def __call__(self, *arg, **kwargs):
- raise RuntimeError('The `%s` function may not be called'
- % self.__name__)
+ raise ConfigureError('The `%s` function may not be called'
+ % self.__name__)
class SandboxedGlobal(dict):
'''Identifiable dict type for use as function global'''
def forbidden_import(*args, **kwargs):
raise ImportError('Importing modules is forbidden')
@@ -102,16 +102,17 @@ class ConfigureSandbox(dict):
'isfile', 'join', 'normpath', 'realpath', 'relpath')
}))
def __init__(self, config, environ=os.environ, argv=sys.argv,
stdout=sys.stdout, stderr=sys.stderr, logger=None):
dict.__setitem__(self, '__builtins__', self.BUILTINS)
self._paths = []
+ self._all_paths = set()
self._templates = set()
# Store the real function and its dependencies, behind each
# DependsFunction generated from @depends.
self._depends = {}
self._seen = set()
# Store the @imports added to a given function.
self._imports = {}
@@ -174,26 +175,28 @@ class ConfigureSandbox(dict):
logger.addHandler(handler)
def exec_file(self, path):
'''Execute one file within the sandbox. Users of this class probably
want to use `run` instead.'''
if self._paths:
path = mozpath.join(mozpath.dirname(self._paths[-1]), path)
+ path = mozpath.normpath(path)
if not mozpath.basedir(path, (mozpath.dirname(self._paths[0]),)):
raise ConfigureError(
'Cannot include `%s` because it is not in a subdirectory '
'of `%s`' % (path, mozpath.dirname(self._paths[0])))
else:
path = mozpath.realpath(mozpath.abspath(path))
- if path in self._paths:
+ if path in self._all_paths:
raise ConfigureError(
'Cannot include `%s` because it was included already.' % path)
self._paths.append(path)
+ self._all_paths.add(path)
source = open(path, 'rb').read()
code = compile(source, path, 'exec')
exec(code, self)
self._paths.pop(-1)
@@ -204,17 +207,17 @@ class ConfigureSandbox(dict):
self.exec_file(path)
# All command line arguments should have been removed (handled) by now.
for arg in self._helper:
without_value = arg.split('=', 1)[0]
if arg in self._implied_options:
frameinfo, reason = self._implied_options[arg]
raise ConfigureError(
- '`%s`, emitted from `%s` line `%d`, was not handled.'
+ '`%s`, emitted from `%s` line %d, is unknown.'
% (without_value, frameinfo[1], frameinfo[2]))
raise InvalidOptionError('Unknown option: %s' % without_value)
# All options must be referenced by some @depends function
for option in self._options.itervalues():
if option not in self._seen:
raise ConfigureError(
'Option `%s` is not handled ; reference it with a @depends'
@@ -270,21 +273,19 @@ class ConfigureSandbox(dict):
be used.
Command line argument/environment variable parsing for this Option is
handled here.
'''
args = [self._resolve(arg) for arg in args]
kwargs = {k: self._resolve(v) for k, v in kwargs.iteritems()}
option = Option(*args, **kwargs)
if option.name in self._options:
- raise ConfigureError('Option `%s` already defined'
- % self._options[option.name].option)
+ raise ConfigureError('Option `%s` already defined' % option.option)
if option.env in self._options:
- raise ConfigureError('Option `%s` already defined'
- % self._options[option.env].option)
+ raise ConfigureError('Option `%s` already defined' % option.env)
if option.name:
self._options[option.name] = option
if option.env:
self._options[option.env] = option
try:
value, option_string = self._helper.handle(option)
except ConflictingOptionError as e:
@@ -341,17 +342,17 @@ class ConfigureSandbox(dict):
elif isinstance(arg, DependsFunction):
assert arg in self._depends
dependencies.append(arg)
arg, _ = self._depends[arg]
resolved_arg = self._results.get(arg)
else:
raise TypeError(
"Cannot use object of type '%s' as argument to @depends"
- % type(arg))
+ % type(arg).__name__)
resolved_args.append(resolved_arg)
dependencies = tuple(dependencies)
def decorator(func):
if inspect.isgeneratorfunction(func):
raise ConfigureError(
'Cannot decorate generator functions with @depends')
func, glob = self._prepare_function(func)
@@ -380,17 +381,17 @@ class ConfigureSandbox(dict):
Allows to include external files for execution in the sandbox.
It is possible to use a @depends function as argument, in which case
the result of the function is the file name to include. This latter
feature is only really meant for --enable-application/--enable-project.
'''
what = self._resolve(what)
if what:
if not isinstance(what, types.StringTypes):
- raise TypeError("Unexpected type: '%s'" % type(what))
+ raise TypeError("Unexpected type: '%s'" % type(what).__name__)
self.exec_file(what)
def template_impl(self, func):
'''Implementation of @template.
This function is a decorator. Template functions are called
immediately. They are altered so that their global namespace exposes
a limited set of functions from os.path, as well as `depends` and
`option`.
@@ -444,26 +445,30 @@ class ConfigureSandbox(dict):
This decorator imports the given _import from the given _from module
optionally under a different _as name.
The options correspond to the various forms for the import builtin.
@imports('sys')
@imports(_from='mozpack', _import='path', _as='mozpath')
'''
for value, required in (
(_import, True), (_from, False), (_as, False)):
- if not isinstance(value, types.StringTypes) and not (
- required or value is None):
- raise TypeError("Unexpected type: '%s'" % type(value))
+
+ if not isinstance(value, types.StringTypes) and (
+ required or value is not None):
+ raise TypeError("Unexpected type: '%s'" % type(value).__name__)
if value is not None and not self.RE_MODULE.match(value):
raise ValueError("Invalid argument to @imports: '%s'" % value)
def decorator(func):
- if func in self._prepared_functions:
+ if func in self._templates:
raise ConfigureError(
- '@imports must appear after other decorators')
+ '@imports must appear after @template')
+ if func in self._depends:
+ raise ConfigureError(
+ '@imports must appear after @depends')
# For the imports to apply in the order they appear in the
# .configure file, we accumulate them in reverse order and apply
# them later.
imports = self._imports.setdefault(func, [])
imports.insert(0, (_from, _import, _as))
return func
return decorator
@@ -498,17 +503,17 @@ class ConfigureSandbox(dict):
def _resolve_and_set(self, data, name, value):
# Don't set anything when --help was on the command line
if self._help:
return
name = self._resolve(name, need_help_dependency=False)
if name is None:
return
if not isinstance(name, types.StringTypes):
- raise TypeError("Unexpected type: '%s'" % type(name))
+ raise TypeError("Unexpected type: '%s'" % type(name).__name__)
if name in data:
raise ConfigureError(
"Cannot add '%s' to configuration: Key already "
"exists" % name)
value = self._resolve(value, need_help_dependency=False)
if value is not None:
data[name] = value
@@ -582,17 +587,17 @@ class ConfigureSandbox(dict):
if not reason and isinstance(value, DependsFunction):
deps = self._depends[value][1]
possible_reasons = [d for d in deps if d != self._help_option]
if len(possible_reasons) == 1:
if isinstance(possible_reasons[0], Option):
reason = (self._raw_options.get(possible_reasons[0]) or
possible_reasons[0].option)
- if not reason or not isinstance(value, DependsFunction):
+ if not reason:
raise ConfigureError(
"Cannot infer what implies '%s'. Please add a `reason` to "
"the `imply_option` call."
% option)
value = self._resolve(value, need_help_dependency=False)
if value is not None:
if isinstance(value, OptionValue):
@@ -601,28 +606,28 @@ class ConfigureSandbox(dict):
value = PositiveOptionValue()
elif value is False or value == ():
value = NegativeOptionValue()
elif isinstance(value, types.StringTypes):
value = PositiveOptionValue((value,))
elif isinstance(value, tuple):
value = PositiveOptionValue(value)
else:
- raise TypeError("Unexpected type: '%s'" % type(value))
+ raise TypeError("Unexpected type: '%s'" % type(value).__name__)
option = value.format(option)
self._helper.add(option, 'implied')
self._implied_options[option] = inspect.stack()[1], reason
def _prepare_function(self, func):
'''Alter the given function global namespace with the common ground
for @depends, and @template.
'''
if not inspect.isfunction(func):
- raise TypeError("Unexpected type: '%s'" % type(func))
+ raise TypeError("Unexpected type: '%s'" % type(func).__name__)
if func in self._prepared_functions:
return func, func.func_globals
glob = SandboxedGlobal(
(k, v) for k, v in func.func_globals.iteritems()
if (inspect.isfunction(v) and v not in self._templates) or (
inspect.isclass(v) and issubclass(v, Exception))
)
--- a/python/mozbuild/mozbuild/test/configure/test_configure.py
+++ b/python/mozbuild/mozbuild/test/configure/test_configure.py
@@ -5,17 +5,20 @@
from __future__ import absolute_import, print_function, unicode_literals
from StringIO import StringIO
import os
import sys
import textwrap
import unittest
-from mozunit import main
+from mozunit import (
+ main,
+ MockedOpen,
+)
from mozbuild.configure.options import (
InvalidOptionError,
NegativeOptionValue,
PositiveOptionValue,
)
from mozbuild.configure import (
ConfigureError,
@@ -36,16 +39,22 @@ class TestConfigure(unittest.TestCase):
sandbox = ConfigureSandbox(config, env, [prog] + options, out, out)
sandbox.run(mozpath.join(test_data_path, configure))
if '--help' not in options:
self.assertEquals('', out.getvalue())
return config
+ def moz_configure(self, source):
+ return MockedOpen({
+ os.path.join(test_data_path,
+ 'moz.configure'): textwrap.dedent(source)
+ })
+
def test_defaults(self):
config = self.get_config()
self.maxDiff = None
self.assertEquals({
'CHOICES': NegativeOptionValue(),
'DEFAULTED': PositiveOptionValue(('not-simple',)),
'IS_GCC': NegativeOptionValue(),
'REMAINDER': (PositiveOptionValue(), NegativeOptionValue(),
@@ -540,11 +549,336 @@ class TestConfigure(unittest.TestCase):
with self.assertRaises(ConfigureError) as e:
self.get_config([], configure='imply_option/infer_ko.configure')
self.assertEquals(
e.exception.message,
"Cannot infer what implies '--enable-bar'. Please add a `reason` "
"to the `imply_option` call.")
+ def test_imply_option_failures(self):
+ with self.assertRaises(ConfigureError) as e:
+ with self.moz_configure('''
+ imply_option('--with-foo', ('a',), 'bar')
+ '''):
+ self.get_config()
+
+ self.assertEquals(e.exception.message,
+ "`--with-foo`, emitted from `%s` line 2, is unknown."
+ % mozpath.join(test_data_path, 'moz.configure'))
+
+ with self.assertRaises(TypeError) as e:
+ with self.moz_configure('''
+ imply_option('--with-foo', 42, 'bar')
+
+ option('--with-foo', help='foo')
+ @depends('--with-foo')
+ def foo(value):
+ return value
+ '''):
+ self.get_config()
+
+ self.assertEquals(e.exception.message,
+ "Unexpected type: 'int'")
+
+ def test_option_failures(self):
+ with self.assertRaises(ConfigureError) as e:
+ with self.moz_configure('option("--with-foo", help="foo")'):
+ self.get_config()
+
+ self.assertEquals(
+ e.exception.message,
+ 'Option `--with-foo` is not handled ; reference it with a @depends'
+ )
+
+ with self.assertRaises(ConfigureError) as e:
+ with self.moz_configure('''
+ option("--with-foo", help="foo")
+ option("--with-foo", help="foo")
+ '''):
+ self.get_config()
+
+ self.assertEquals(
+ e.exception.message,
+ 'Option `--with-foo` already defined'
+ )
+
+ with self.assertRaises(ConfigureError) as e:
+ with self.moz_configure('''
+ option(env="MOZ_FOO", help="foo")
+ option(env="MOZ_FOO", help="foo")
+ '''):
+ self.get_config()
+
+ self.assertEquals(
+ e.exception.message,
+ 'Option `MOZ_FOO` already defined'
+ )
+
+ with self.assertRaises(ConfigureError) as e:
+ with self.moz_configure('''
+ option('--with-foo', env="MOZ_FOO", help="foo")
+ option(env="MOZ_FOO", help="foo")
+ '''):
+ self.get_config()
+
+ self.assertEquals(
+ e.exception.message,
+ 'Option `MOZ_FOO` already defined'
+ )
+
+ with self.assertRaises(ConfigureError) as e:
+ with self.moz_configure('''
+ option(env="MOZ_FOO", help="foo")
+ option('--with-foo', env="MOZ_FOO", help="foo")
+ '''):
+ self.get_config()
+
+ self.assertEquals(
+ e.exception.message,
+ 'Option `MOZ_FOO` already defined'
+ )
+
+ with self.assertRaises(ConfigureError) as e:
+ with self.moz_configure('''
+ option('--with-foo', env="MOZ_FOO", help="foo")
+ option('--with-foo', help="foo")
+ '''):
+ self.get_config()
+
+ self.assertEquals(
+ e.exception.message,
+ 'Option `--with-foo` already defined'
+ )
+
+ def test_include_failures(self):
+ with self.assertRaises(ConfigureError) as e:
+ with self.moz_configure('include("../foo.configure")'):
+ self.get_config()
+
+ self.assertEquals(
+ e.exception.message,
+ 'Cannot include `%s` because it is not in a subdirectory of `%s`'
+ % (mozpath.normpath(mozpath.join(test_data_path, '..',
+ 'foo.configure')),
+ mozpath.normsep(test_data_path))
+ )
+
+ with self.assertRaises(ConfigureError) as e:
+ with self.moz_configure('''
+ include('extra.configure')
+ include('extra.configure')
+ '''):
+ self.get_config()
+
+ self.assertEquals(
+ e.exception.message,
+ 'Cannot include `%s` because it was included already.'
+ % mozpath.normpath(mozpath.join(test_data_path,
+ 'extra.configure'))
+ )
+
+ with self.assertRaises(TypeError) as e:
+ with self.moz_configure('''
+ include(42)
+ '''):
+ self.get_config()
+
+ self.assertEquals(e.exception.message, "Unexpected type: 'int'")
+
+ def test_sandbox_failures(self):
+ with self.assertRaises(KeyError) as e:
+ with self.moz_configure('''
+ include = 42
+ '''):
+ self.get_config()
+
+ self.assertEquals(e.exception.message, 'Cannot reassign builtins')
+
+ with self.assertRaises(KeyError) as e:
+ with self.moz_configure('''
+ foo = 42
+ '''):
+ self.get_config()
+
+ self.assertEquals(e.exception.message,
+ 'Cannot assign `foo` because it is neither a '
+ '@depends nor a @template')
+
+ def test_depends_failures(self):
+ with self.assertRaises(ConfigureError) as e:
+ with self.moz_configure('''
+ @depends()
+ def foo():
+ return
+ '''):
+ self.get_config()
+
+ self.assertEquals(e.exception.message,
+ "@depends needs at least one argument")
+
+ with self.assertRaises(ConfigureError) as e:
+ with self.moz_configure('''
+ @depends('--with-foo')
+ def foo(value):
+ return value
+ '''):
+ self.get_config()
+
+ self.assertEquals(e.exception.message,
+ "'--with-foo' is not a known option. Maybe it's "
+ "declared too late?")
+
+ with self.assertRaises(ConfigureError) as e:
+ with self.moz_configure('''
+ @depends('--with-foo=42')
+ def foo(value):
+ return value
+ '''):
+ self.get_config()
+
+ self.assertEquals(e.exception.message,
+ "Option must not contain an '='")
+
+ with self.assertRaises(TypeError) as e:
+ with self.moz_configure('''
+ @depends(42)
+ def foo(value):
+ return value
+ '''):
+ self.get_config()
+
+ self.assertEquals(e.exception.message,
+ "Cannot use object of type 'int' as argument "
+ "to @depends")
+
+ with self.assertRaises(ConfigureError) as e:
+ with self.moz_configure('''
+ @depends('--help')
+ def foo(value):
+ yield
+ '''):
+ self.get_config()
+
+ self.assertEquals(e.exception.message,
+ "Cannot decorate generator functions with @depends")
+
+ with self.assertRaises(ConfigureError) as e:
+ with self.moz_configure('''
+ option('--foo', help='foo')
+ @depends('--foo')
+ def foo(value):
+ return value
+
+ @depends('--help', foo)
+ def bar(help, foo):
+ return
+ '''):
+ self.get_config()
+
+ self.assertEquals(e.exception.message,
+ "`bar` depends on '--help' and `foo`. "
+ "`foo` must depend on '--help'")
+
+ with self.assertRaises(TypeError) as e:
+ with self.moz_configure('''
+ depends('--help')(42)
+ '''):
+ self.get_config()
+
+ self.assertEquals(e.exception.message,
+ "Unexpected type: 'int'")
+
+ with self.assertRaises(ConfigureError) as e:
+ with self.moz_configure('''
+ option('--foo', help='foo')
+ @depends('--foo')
+ def foo(value):
+ return value
+
+ include(foo)
+ '''):
+ self.get_config()
+
+ self.assertEquals(e.exception.message,
+ "Missing @depends for `foo`: '--help'")
+
+ with self.assertRaises(ConfigureError) as e:
+ with self.moz_configure('''
+ option('--foo', help='foo')
+ @depends('--foo')
+ def foo(value):
+ return value
+
+ foo()
+ '''):
+ self.get_config()
+
+ self.assertEquals(e.exception.message,
+ "The `foo` function may not be called")
+
+ def test_imports_failures(self):
+ with self.assertRaises(ConfigureError) as e:
+ with self.moz_configure('''
+ @imports('os')
+ @template
+ def foo(value):
+ return value
+ '''):
+ self.get_config()
+
+ self.assertEquals(e.exception.message,
+ '@imports must appear after @template')
+
+ with self.assertRaises(ConfigureError) as e:
+ with self.moz_configure('''
+ option('--foo', help='foo')
+ @imports('os')
+ @depends('--foo')
+ def foo(value):
+ return value
+ '''):
+ self.get_config()
+
+ self.assertEquals(e.exception.message,
+ '@imports must appear after @depends')
+
+ for import_ in (
+ "42",
+ "_from=42, _import='os'",
+ "_from='os', _import='path', _as=42",
+ ):
+ with self.assertRaises(TypeError) as e:
+ with self.moz_configure('''
+ @imports(%s)
+ @template
+ def foo(value):
+ return value
+ ''' % import_):
+ self.get_config()
+
+ self.assertEquals(e.exception.message, "Unexpected type: 'int'")
+
+ with self.assertRaises(TypeError) as e:
+ with self.moz_configure('''
+ @imports('os', 42)
+ @template
+ def foo(value):
+ return value
+ '''):
+ self.get_config()
+
+ self.assertEquals(e.exception.message, "Unexpected type: 'int'")
+
+ with self.assertRaises(ValueError) as e:
+ with self.moz_configure('''
+ @imports('os*')
+ def foo(value):
+ return value
+ '''):
+ self.get_config()
+
+ self.assertEquals(e.exception.message,
+ "Invalid argument to @imports: 'os*'")
+
if __name__ == '__main__':
main()