Bug 1254374 - Add various failure tests to test_configure.py. r?nalexander draft
authorMike Hommey <mh+mozilla@glandium.org>
Tue, 12 Apr 2016 17:26:46 +0900
changeset 349793 cf822f3601290cd6f68d3e69eb0a64369baf1a79
parent 349792 63f0979433d09595d493c16fbcfc6af331d34ba8
child 518184 28bc017f150c55f371c631ac034628d64772feb5
push id15177
push userbmo:mh+mozilla@glandium.org
push dateTue, 12 Apr 2016 09:20:20 +0000
reviewersnalexander
bugs1254374
milestone48.0a1
Bug 1254374 - Add various failure tests to test_configure.py. r?nalexander At the same time, improve some of the failures handling paths.
python/mozbuild/mozbuild/configure/__init__.py
python/mozbuild/mozbuild/test/configure/test_configure.py
--- 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()