Bug 1270446 - Make it easier to derive compiler results. r?chmanchester draft
authorMike Hommey <mh+mozilla@glandium.org>
Fri, 22 Apr 2016 16:48:14 +0900
changeset 363769 4683e524cf4b9afca71f47679be8c3b443fd0f36
parent 363768 8cc149414b965c38f0c0785bd11d5dcceb85cd8a
child 363770 a3026d748faffab35e076dbd2f110d97aea12409
push id17288
push userbmo:mh+mozilla@glandium.org
push dateThu, 05 May 2016 11:06:21 +0000
reviewerschmanchester
bugs1270446
milestone49.0a1
Bug 1270446 - Make it easier to derive compiler results. r?chmanchester
python/mozbuild/mozbuild/test/configure/test_toolchain_configure.py
python/mozbuild/mozbuild/test/configure/test_toolchain_helpers.py
python/mozbuild/mozbuild/util.py
--- a/python/mozbuild/mozbuild/test/configure/test_toolchain_configure.py
+++ b/python/mozbuild/mozbuild/test/configure/test_toolchain_configure.py
@@ -10,17 +10,20 @@ import os
 from StringIO import StringIO
 
 from mozunit import main
 
 from common import BaseConfigureTest
 from mozbuild.configure.util import Version
 from mozbuild.util import memoize
 from mozpack import path as mozpath
-from test_toolchain_helpers import FakeCompiler
+from test_toolchain_helpers import (
+    FakeCompiler,
+    CompilerResult,
+)
 
 
 DEFAULT_C99 = {
     '__STDC_VERSION__': '199901L',
 }
 
 DEFAULT_C11 = {
     '__STDC_VERSION__': '201112L',
@@ -152,21 +155,18 @@ class BaseToolchainTest(BaseConfigureTes
     def do_toolchain_test(self, paths, results, args=[], environ={}):
         '''Helper to test the toolchain checks from toolchain.configure.
 
         - `paths` is a dict associating compiler paths to FakeCompiler
           definitions from above.
         - `results` is a dict associating result variable names from
           toolchain.configure (c_compiler, cxx_compiler, host_c_compiler,
           host_cxx_compiler) with a result.
-          The result can either be an error string, or a dict with the
-          following items: flags, version, type, compiler, wrapper. (wrapper
-          can be omitted when it's empty). Those items correspond to the
-          attributes of the object returned by toolchain.configure checks
-          and will be compared to them.
+          The result can either be an error string, or a CompilerResult
+          corresponding to the object returned by toolchain.configure checks.
           When the results for host_c_compiler are identical to c_compiler,
           they can be omitted. Likewise for host_cxx_compiler vs.
           cxx_compiler.
         '''
         environ = dict(environ)
         if 'PATH' not in environ:
             environ['PATH'] = os.pathsep.join(
                 mozpath.abspath(p) for p in ('/bin', '/usr/bin'))
@@ -177,26 +177,22 @@ class BaseToolchainTest(BaseConfigureTes
         for var in ('c_compiler', 'cxx_compiler', 'host_c_compiler',
                     'host_cxx_compiler'):
             if var in results:
                 result = results[var]
             elif var.startswith('host_'):
                 result = results.get(var[5:], {})
             else:
                 result = {}
-            if isinstance(result, dict):
-                result = dict(result)
-                result.setdefault('wrapper', [])
-                result['compiler'] = mozpath.abspath(result['compiler'])
             try:
                 self.out.truncate(0)
                 compiler = sandbox._value_for(sandbox[var])
                 # Add var on both ends to make it clear which of the
                 # variables is failing the test when that happens.
-                self.assertEquals((var, compiler.__dict__), (var, result))
+                self.assertEquals((var, compiler), (var, result))
             except SystemExit:
                 self.assertEquals((var, result),
                                   (var, self.out.getvalue().strip()))
                 return
 
 
 class LinuxToolchainTest(BaseToolchainTest):
     PATHS = {
@@ -211,59 +207,59 @@ class LinuxToolchainTest(BaseToolchainTe
         '/usr/bin/clang-3.6': CLANG_3_6,
         '/usr/bin/clang++-3.6': CLANGXX_3_6,
         '/usr/bin/clang-3.3': CLANG_3_3,
         '/usr/bin/clang++-3.3': CLANGXX_3_3,
     }
     GCC_4_7_RESULT = ('Only GCC 4.8 or newer is supported '
                       '(found version 4.7.3).')
     GXX_4_7_RESULT = GCC_4_7_RESULT
-    GCC_4_9_RESULT = {
-        'flags': ['-std=gnu99'],
-        'version': '4.9.3',
-        'type': 'gcc',
-        'compiler': '/usr/bin/gcc',
-    }
-    GXX_4_9_RESULT = {
-        'flags': ['-std=gnu++11'],
-        'version': '4.9.3',
-        'type': 'gcc',
-        'compiler': '/usr/bin/g++',
-    }
-    GCC_5_RESULT = {
-        'flags': ['-std=gnu99'],
-        'version': '5.2.1',
-        'type': 'gcc',
-        'compiler': '/usr/bin/gcc-5',
-    }
-    GXX_5_RESULT = {
-        'flags': ['-std=gnu++11'],
-        'version': '5.2.1',
-        'type': 'gcc',
-        'compiler': '/usr/bin/g++-5',
-    }
-    CLANG_3_3_RESULT = {
-        'flags': [],
-        'version': '3.3.0',
-        'type': 'clang',
-        'compiler': '/usr/bin/clang-3.3',
-    }
+    GCC_4_9_RESULT = CompilerResult(
+        flags=['-std=gnu99'],
+        version='4.9.3',
+        type='gcc',
+        compiler='/usr/bin/gcc',
+    )
+    GXX_4_9_RESULT = CompilerResult(
+        flags=['-std=gnu++11'],
+        version='4.9.3',
+        type='gcc',
+        compiler='/usr/bin/g++',
+    )
+    GCC_5_RESULT = CompilerResult(
+        flags=['-std=gnu99'],
+        version='5.2.1',
+        type='gcc',
+        compiler='/usr/bin/gcc-5',
+    )
+    GXX_5_RESULT = CompilerResult(
+        flags=['-std=gnu++11'],
+        version='5.2.1',
+        type='gcc',
+        compiler='/usr/bin/g++-5',
+    )
+    CLANG_3_3_RESULT = CompilerResult(
+        flags=[],
+        version='3.3.0',
+        type='clang',
+        compiler='/usr/bin/clang-3.3',
+    )
     CLANGXX_3_3_RESULT = 'Only clang/llvm 3.4 or newer is supported.'
-    CLANG_3_6_RESULT = {
-        'flags': ['-std=gnu99'],
-        'version': '3.6.2',
-        'type': 'clang',
-        'compiler': '/usr/bin/clang',
-    }
-    CLANGXX_3_6_RESULT = {
-        'flags': ['-std=gnu++11'],
-        'version': '3.6.2',
-        'type': 'clang',
-        'compiler': '/usr/bin/clang++',
-    }
+    CLANG_3_6_RESULT = CompilerResult(
+        flags=['-std=gnu99'],
+        version='3.6.2',
+        type='clang',
+        compiler='/usr/bin/clang',
+    )
+    CLANGXX_3_6_RESULT = CompilerResult(
+        flags=['-std=gnu++11'],
+        version='3.6.2',
+        type='clang',
+        compiler='/usr/bin/clang++',
+    )
 
     def test_gcc(self):
         # We'll try gcc and clang, and find gcc first.
         self.do_toolchain_test(self.PATHS, {
             'c_compiler': self.GCC_4_9_RESULT,
             'cxx_compiler': self.GXX_4_9_RESULT,
         })
 
@@ -369,23 +365,23 @@ class LinuxToolchainTest(BaseToolchainTe
         }
         self.do_toolchain_test(paths, {
             'c_compiler': self.CLANG_3_6_RESULT,
             'cxx_compiler': self.CLANGXX_3_6_RESULT,
         })
 
     def test_guess_cxx_clang(self):
         # When CXX is not set, we guess it from CC.
-        clang_3_6_result = dict(self.CLANG_3_6_RESULT)
-        clang_3_6_result['compiler'] = '/usr/bin/clang-3.6'
-        clangxx_3_6_result = dict(self.CLANGXX_3_6_RESULT)
-        clangxx_3_6_result['compiler'] = '/usr/bin/clang++-3.6'
         self.do_toolchain_test(self.PATHS, {
-            'c_compiler': clang_3_6_result,
-            'cxx_compiler': clangxx_3_6_result,
+            'c_compiler': self.CLANG_3_6_RESULT + {
+                'compiler': '/usr/bin/clang-3.6',
+            },
+            'cxx_compiler': self.CLANGXX_3_6_RESULT + {
+                'compiler': '/usr/bin/clang++-3.6',
+            },
         }, environ={
             'CC': 'clang-3.6',
         })
 
     def test_unsupported_clang(self):
         # clang 3.3 C compiler is perfectly fine, but we need more for C++.
         self.do_toolchain_test(self.PATHS, {
             'c_compiler': self.CLANG_3_3_RESULT,
@@ -407,46 +403,46 @@ class LinuxToolchainTest(BaseToolchainTe
         })
 
     def test_absolute_path(self):
         paths = dict(self.PATHS)
         paths.update({
             '/opt/clang/bin/clang': CLANG_3_6,
             '/opt/clang/bin/clang++': CLANGXX_3_6,
         })
-        absolute_clang = dict(self.CLANG_3_6_RESULT)
-        absolute_clang['compiler'] = '/opt/clang/bin/clang'
-        absolute_clangxx = dict(self.CLANGXX_3_6_RESULT)
-        absolute_clangxx['compiler'] = '/opt/clang/bin/clang++'
         result = {
-            'c_compiler': absolute_clang,
-            'cxx_compiler': absolute_clangxx,
+            'c_compiler': self.CLANG_3_6_RESULT + {
+                'compiler': '/opt/clang/bin/clang',
+            },
+            'cxx_compiler': self.CLANGXX_3_6_RESULT + {
+                'compiler': '/opt/clang/bin/clang++'
+            },
         }
         self.do_toolchain_test(paths, result, environ={
             'CC': '/opt/clang/bin/clang',
             'CXX': '/opt/clang/bin/clang++',
         })
         # With CXX guess too.
         self.do_toolchain_test(paths, result, environ={
             'CC': '/opt/clang/bin/clang',
         })
 
     def test_atypical_name(self):
         paths = dict(self.PATHS)
         paths.update({
             '/usr/bin/afl-clang-fast': CLANG_3_6,
             '/usr/bin/afl-clang-fast++': CLANGXX_3_6,
         })
-        afl_clang_result = dict(self.CLANG_3_6_RESULT)
-        afl_clang_result['compiler'] = '/usr/bin/afl-clang-fast'
-        afl_clangxx_result = dict(self.CLANGXX_3_6_RESULT)
-        afl_clangxx_result['compiler'] = '/usr/bin/afl-clang-fast++'
         self.do_toolchain_test(paths, {
-            'c_compiler': afl_clang_result,
-            'cxx_compiler': afl_clangxx_result,
+            'c_compiler': self.CLANG_3_6_RESULT + {
+                'compiler': '/usr/bin/afl-clang-fast',
+            },
+            'cxx_compiler': self.CLANGXX_3_6_RESULT + {
+                'compiler': '/usr/bin/afl-clang-fast++',
+            },
         }, environ={
             'CC': 'afl-clang-fast',
             'CXX': 'afl-clang-fast++',
         })
 
     def test_mixed_compilers(self):
         self.do_toolchain_test(self.PATHS, {
             'c_compiler': self.CLANG_3_6_RESULT,
@@ -541,46 +537,46 @@ class WindowsToolchainTest(BaseToolchain
     }
     PATHS.update(LinuxToolchainTest.PATHS)
 
     VS_2013u2_RESULT = (
         'This version (18.00.30501) of the MSVC compiler is not supported.\n'
         'You must install Visual C++ 2013 Update 3, Visual C++ 2015 Update 1, '
         'or newer in order to build.\n'
         'See https://developer.mozilla.org/en/Windows_Build_Prerequisites')
-    VS_2013u3_RESULT = {
-        'flags': [],
-        'version': '18.00.30723',
-        'type': 'msvc',
-        'compiler': '/opt/VS_2013u3/bin/cl',
-    }
+    VS_2013u3_RESULT = CompilerResult(
+        flags=[],
+        version='18.00.30723',
+        type='msvc',
+        compiler='/opt/VS_2013u3/bin/cl',
+    )
     VS_2015_RESULT = (
         'This version (19.00.23026) of the MSVC compiler is not supported.\n'
         'You must install Visual C++ 2013 Update 3, Visual C++ 2015 Update 1, '
         'or newer in order to build.\n'
         'See https://developer.mozilla.org/en/Windows_Build_Prerequisites')
-    VS_2015u1_RESULT = {
-        'flags': [],
-        'version': '19.00.23506',
-        'type': 'msvc',
-        'compiler': '/usr/bin/cl',
-    }
-    CLANG_CL_3_9_RESULT = {
-        'flags': ['-Xclang', '-std=gnu99',
-                  '-fms-compatibility-version=18.00.30723', '-fallback'],
-        'version': '18.00.30723',
-        'type': 'clang-cl',
-        'compiler': '/usr/bin/clang-cl',
-    }
-    CLANGXX_CL_3_9_RESULT = {
-        'flags': ['-fms-compatibility-version=18.00.30723', '-fallback'],
-        'version': '18.00.30723',
-        'type': 'clang-cl',
-        'compiler': '/usr/bin/clang-cl',
-    }
+    VS_2015u1_RESULT = CompilerResult(
+        flags=[],
+        version='19.00.23506',
+        type='msvc',
+        compiler='/usr/bin/cl',
+    )
+    CLANG_CL_3_9_RESULT = CompilerResult(
+        flags=['-Xclang', '-std=gnu99',
+               '-fms-compatibility-version=18.00.30723', '-fallback'],
+        version='18.00.30723',
+        type='clang-cl',
+        compiler='/usr/bin/clang-cl',
+    )
+    CLANGXX_CL_3_9_RESULT = CompilerResult(
+        flags=['-fms-compatibility-version=18.00.30723', '-fallback'],
+        version='18.00.30723',
+        type='clang-cl',
+        compiler='/usr/bin/clang-cl',
+    )
     CLANG_3_3_RESULT = LinuxToolchainTest.CLANG_3_3_RESULT
     CLANGXX_3_3_RESULT = LinuxToolchainTest.CLANGXX_3_3_RESULT
     CLANG_3_6_RESULT = LinuxToolchainTest.CLANG_3_6_RESULT
     CLANGXX_3_6_RESULT = LinuxToolchainTest.CLANGXX_3_6_RESULT
     GCC_4_7_RESULT = LinuxToolchainTest.GCC_4_7_RESULT
     GCC_4_9_RESULT = LinuxToolchainTest.GCC_4_9_RESULT
     GXX_4_9_RESULT = LinuxToolchainTest.GXX_4_9_RESULT
     GCC_5_RESULT = LinuxToolchainTest.GCC_5_RESULT
@@ -672,34 +668,36 @@ class CrossCompileToolchainTest(BaseTool
         '/usr/bin/arm-linux-gnu-gcc': GCC_4_9,
         '/usr/bin/arm-linux-gnu-g++': GXX_4_9,
         '/usr/bin/arm-linux-gnu-gcc-4.7': GCC_4_7,
         '/usr/bin/arm-linux-gnu-g++-4.7': GXX_4_7,
         '/usr/bin/arm-linux-gnu-gcc-5': GCC_5,
         '/usr/bin/arm-linux-gnu-g++-5': GXX_5,
     }
     PATHS.update(LinuxToolchainTest.PATHS)
-    ARM_GCC_4_9_RESULT = dict(LinuxToolchainTest.GCC_4_9_RESULT)
-    ARM_GCC_4_9_RESULT['compiler'] = '/usr/bin/arm-linux-gnu-gcc'
-    ARM_GXX_4_9_RESULT = dict(LinuxToolchainTest.GXX_4_9_RESULT)
-    ARM_GXX_4_9_RESULT['compiler'] = '/usr/bin/arm-linux-gnu-g++'
     ARM_GCC_4_7_RESULT = LinuxToolchainTest.GXX_4_7_RESULT
-    ARM_GCC_5_RESULT = dict(LinuxToolchainTest.GCC_5_RESULT)
-    ARM_GCC_5_RESULT['compiler'] = '/usr/bin/arm-linux-gnu-gcc-5'
-    ARM_GXX_5_RESULT = dict(LinuxToolchainTest.GXX_5_RESULT)
-    ARM_GXX_5_RESULT['compiler'] = '/usr/bin/arm-linux-gnu-g++-5'
+    ARM_GCC_5_RESULT = LinuxToolchainTest.GCC_5_RESULT + {
+        'compiler': '/usr/bin/arm-linux-gnu-gcc-5',
+    }
+    ARM_GXX_5_RESULT = LinuxToolchainTest.GXX_5_RESULT + {
+        'compiler': '/usr/bin/arm-linux-gnu-g++-5',
+    }
     CLANG_3_6_RESULT = LinuxToolchainTest.CLANG_3_6_RESULT
     CLANGXX_3_6_RESULT = LinuxToolchainTest.CLANGXX_3_6_RESULT
     GCC_4_9_RESULT = LinuxToolchainTest.GCC_4_9_RESULT
     GXX_4_9_RESULT = LinuxToolchainTest.GXX_4_9_RESULT
 
     def test_cross_gcc(self):
         self.do_toolchain_test(self.PATHS, {
-            'c_compiler': self.ARM_GCC_4_9_RESULT,
-            'cxx_compiler': self.ARM_GXX_4_9_RESULT,
+            'c_compiler': self.GCC_4_9_RESULT + {
+                'compiler': '/usr/bin/arm-linux-gnu-gcc',
+            },
+            'cxx_compiler': self.GXX_4_9_RESULT + {
+                'compiler': '/usr/bin/arm-linux-gnu-g++',
+            },
             'host_c_compiler': self.GCC_4_9_RESULT,
             'host_cxx_compiler': self.GXX_4_9_RESULT,
         }, args=['--target=arm-unknown-linux-gnu'])
 
     def test_overridden_cross_gcc(self):
         self.do_toolchain_test(self.PATHS, {
             'c_compiler': self.ARM_GCC_5_RESULT,
             'cxx_compiler': self.ARM_GXX_5_RESULT,
--- a/python/mozbuild/mozbuild/test/configure/test_toolchain_helpers.py
+++ b/python/mozbuild/mozbuild/test/configure/test_toolchain_helpers.py
@@ -1,28 +1,31 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import, print_function, unicode_literals
 
+import copy
 import re
 import types
 import unittest
 
 from fnmatch import fnmatch
 from StringIO import StringIO
 from textwrap import dedent
 
 from mozunit import (
     main,
     MockedOpen,
 )
 
 from mozbuild.preprocessor import Preprocessor
+from mozbuild.util import ReadOnlyNamespace
+from mozpack import path as mozpath
 
 
 class CompilerPreprocessor(Preprocessor):
     VARSUBST = re.compile('(?P<VAR>\w+)', re.U)
     NON_WHITESPACE = re.compile('\S')
 
     def __init__(self, *args, **kwargs):
         Preprocessor.__init__(self, *args, **kwargs)
@@ -320,10 +323,93 @@ class TestFakeCompiler(unittest.TestCase
                 'D': 5,
             },
             '-bar': {
                 'E': 6,
             },
         })
 
 
+class CompilerResult(ReadOnlyNamespace):
+    '''Helper of convenience to manipulate toolchain results in unit tests
+
+    When adding a dict, the result is a new CompilerResult with the values
+    from the dict replacing those from the CompilerResult, except for `flags`,
+    where the value from the dict extends the `flags` in `self`.
+    '''
+
+    def __init__(self, wrapper=None, compiler='', version='', type='',
+                 flags=None):
+        if flags is None:
+            flags = []
+        if wrapper is None:
+            wrapper = []
+        super(CompilerResult, self).__init__(
+            flags=flags,
+            version=version,
+            type=type,
+            compiler=mozpath.abspath(compiler),
+            wrapper=wrapper,
+        )
+
+    def __add__(self, other):
+        assert isinstance(other, dict)
+        result = copy.deepcopy(self.__dict__)
+        for k, v in other.iteritems():
+            if k == 'flags':
+                result.setdefault(k, []).extend(v)
+            else:
+                result[k] = v
+        return CompilerResult(**result)
+
+
+class TestCompilerResult(unittest.TestCase):
+    def test_compiler_result(self):
+        result = CompilerResult()
+        self.assertEquals(result.__dict__, {
+            'wrapper': [],
+            'compiler': mozpath.abspath(''),
+            'version': '',
+            'type': '',
+            'flags': [],
+        })
+
+        result = CompilerResult(
+            compiler='/usr/bin/gcc',
+            version='4.2.1',
+            type='gcc',
+            flags=['-std=gnu99'],
+        )
+        self.assertEquals(result.__dict__, {
+            'wrapper': [],
+            'compiler': mozpath.abspath('/usr/bin/gcc'),
+            'version': '4.2.1',
+            'type': 'gcc',
+            'flags': ['-std=gnu99'],
+        })
+
+        result2 = result + {'flags': ['-m32']}
+        self.assertEquals(result2.__dict__, {
+            'wrapper': [],
+            'compiler': mozpath.abspath('/usr/bin/gcc'),
+            'version': '4.2.1',
+            'type': 'gcc',
+            'flags': ['-std=gnu99', '-m32'],
+        })
+        # Original flags are untouched.
+        self.assertEquals(result.flags, ['-std=gnu99'])
+
+        result3 = result + {
+            'compiler': '/usr/bin/gcc-4.7',
+            'version': '4.7.3',
+            'flags': ['-m32'],
+        }
+        self.assertEquals(result3.__dict__, {
+            'wrapper': [],
+            'compiler': mozpath.abspath('/usr/bin/gcc-4.7'),
+            'version': '4.7.3',
+            'type': 'gcc',
+            'flags': ['-std=gnu99', '-m32'],
+        })
+
+
 if __name__ == '__main__':
     main()
--- a/python/mozbuild/mozbuild/util.py
+++ b/python/mozbuild/mozbuild/util.py
@@ -101,16 +101,19 @@ class ReadOnlyNamespace(object):
         raise Exception('Object does not support assignment.')
 
     def __ne__(self, other):
         return not (self == other)
 
     def __eq__(self, other):
         return self is other or self.__dict__ == other.__dict__
 
+    def __repr__(self):
+        return '<%s %r>' % (self.__class__.__name__, self.__dict__)
+
 
 class ReadOnlyDict(dict):
     """A read-only dictionary."""
     def __init__(self, *args, **kwargs):
         dict.__init__(self, *args, **kwargs)
 
     def __delitem__(self, key):
         raise Exception('Object does not support deletion.')