Bug 1254374 - Add various failure tests to test_configure.py. r=nalexander
authorMike Hommey <mh+mozilla@glandium.org>
Tue, 12 Apr 2016 17:26:46 +0900
changeset 330790 7edca44ff30cfedd791f7cbfd6e88b4743c94e71
parent 330789 571153e117bcc644af2de1644065edc2cf31abd0
child 330791 55b2f91e705dc6a6938b3c53b7981fca537b19e1
push id6048
push userkmoir@mozilla.com
push dateMon, 06 Jun 2016 19:02:08 +0000
treeherdermozilla-beta@46d72a56c57d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnalexander
bugs1254374
milestone48.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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()