Bug 1266343 - Add unit tests for the toolchain checks. r?chmanchester draft
authorMike Hommey <mh+mozilla@glandium.org>
Wed, 20 Apr 2016 13:56:55 +0900
changeset 354786 41a964b1a8a051d78ae98198925638478aecd5d1
parent 354785 d5fc3fc9d3437bc96d5a0d57ecede8bc215637a0
child 519066 c67b0a6c2f03cd1890468c91f476672855a38d2a
push id16148
push userbmo:mh+mozilla@glandium.org
push dateThu, 21 Apr 2016 12:30:02 +0000
reviewerschmanchester
bugs1266343
milestone48.0a1
Bug 1266343 - Add unit tests for the toolchain checks. r?chmanchester
build/moz.configure/toolchain.configure
python/moz.build
python/mozbuild/mozbuild/test/configure/common.py
python/mozbuild/mozbuild/test/configure/test_toolchain_configure.py
--- a/build/moz.configure/toolchain.configure
+++ b/build/moz.configure/toolchain.configure
@@ -543,29 +543,30 @@ def compiler(language, host_or_target, c
 
     @depends(valid_compiler)
     @checking('%s version' % what)
     def compiler_version(compiler):
         return compiler.version
 
     if language == 'C++':
         @depends(valid_compiler, c_compiler)
-        def compiler_suite_consistency(compiler, c_compiler):
+        def valid_compiler(compiler, c_compiler):
             if compiler.type != c_compiler.type:
                 die('The %s C compiler is %s, while the %s C++ compiler is '
                     '%s. Need to use the same compiler suite.',
                     host_or_target_str, c_compiler.type,
                     host_or_target_str, compiler.type)
 
             if compiler.version != c_compiler.version:
                 die('The %s C compiler is version %s, while the %s C++ '
                     'compiler is version %s. Need to use the same compiler '
                     'version.',
                     host_or_target_str, c_compiler.version,
                     host_or_target_str, compiler.version)
+            return compiler
 
     # Set CC/CXX/HOST_CC/HOST_CXX for old-configure, which needs the wrapper
     # and the flags that were part of the user input for those variables to
     # be provided.
     add_old_configure_assignment(var, depends_if(valid_compiler)(
         lambda x: list(x.wrapper) + [x.compiler] + list(x.flags)))
 
     # Set CC_TYPE/CC_VERSION/HOST_CC_TYPE/HOST_CC_VERSION to allow
--- a/python/moz.build
+++ b/python/moz.build
@@ -35,16 +35,17 @@ PYTHON_UNIT_TESTS += [
     'mozbuild/mozbuild/test/backend/test_configenvironment.py',
     'mozbuild/mozbuild/test/backend/test_recursivemake.py',
     'mozbuild/mozbuild/test/backend/test_visualstudio.py',
     'mozbuild/mozbuild/test/compilation/test_warnings.py',
     'mozbuild/mozbuild/test/configure/test_checks_configure.py',
     'mozbuild/mozbuild/test/configure/test_configure.py',
     'mozbuild/mozbuild/test/configure/test_moz_configure.py',
     'mozbuild/mozbuild/test/configure/test_options.py',
+    'mozbuild/mozbuild/test/configure/test_toolchain_configure.py',
     'mozbuild/mozbuild/test/configure/test_util.py',
     'mozbuild/mozbuild/test/controller/test_ccachestats.py',
     'mozbuild/mozbuild/test/controller/test_clobber.py',
     'mozbuild/mozbuild/test/frontend/test_context.py',
     'mozbuild/mozbuild/test/frontend/test_emitter.py',
     'mozbuild/mozbuild/test/frontend/test_namespaces.py',
     'mozbuild/mozbuild/test/frontend/test_reader.py',
     'mozbuild/mozbuild/test/frontend/test_sandbox.py',
--- a/python/mozbuild/mozbuild/test/configure/common.py
+++ b/python/mozbuild/mozbuild/test/configure/common.py
@@ -161,19 +161,25 @@ class BaseConfigureTest(unittest.TestCas
 
     def config_guess(self, stdin, args):
         return 0, self.HOST, ''
 
     def config_sub(self, stdin, args):
         return 0, args[0], ''
 
     def get_sandbox(self, paths, config, args=[], environ={}, mozconfig='',
-                    out=None):
-        if not out:
-            out = StringIO()
+                    out=None, logger=None):
+        kwargs = {}
+        if logger:
+            kwargs['logger'] = logger
+        else:
+            if not out:
+                out = StringIO()
+            kwargs['stdout'] = out
+            kwargs['stderr'] = out
 
         if mozconfig:
             fh, mozconfig_path = tempfile.mkstemp()
             os.write(fh, mozconfig)
             os.close(fh)
         else:
             mozconfig_path = os.path.join(os.path.dirname(__file__), 'data',
                                           'empty_mozconfig')
@@ -186,15 +192,15 @@ class BaseConfigureTest(unittest.TestCas
 
             paths = dict(paths)
             autoconf_dir = mozpath.join(topsrcdir, 'build', 'autoconf')
             paths[mozpath.join(autoconf_dir,
                                'config.guess')] = self.config_guess
             paths[mozpath.join(autoconf_dir, 'config.sub')] = self.config_sub
 
             sandbox = ConfigureTestSandbox(paths, config, environ,
-                                           ['configure'] + args, out, out)
+                                           ['configure'] + args, **kwargs)
             sandbox.include_file(os.path.join(topsrcdir, 'moz.configure'))
 
             return sandbox
         finally:
             if mozconfig:
                 os.remove(mozconfig_path)
new file mode 100644
--- /dev/null
+++ b/python/mozbuild/mozbuild/test/configure/test_toolchain_configure.py
@@ -0,0 +1,1026 @@
+# 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 logging
+import os
+import re
+import types
+import unittest
+
+from collections import defaultdict
+from fnmatch import fnmatch
+from StringIO import StringIO
+from textwrap import dedent
+
+from mozunit import (
+    main,
+    MockedOpen,
+)
+
+from common import BaseConfigureTest
+from mozbuild.preprocessor import Preprocessor
+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)
+        self.do_filter('c_substitution')
+        self.setMarker('#\s*')
+
+    def do_if(self, *args, **kwargs):
+        # The C preprocessor handles numbers following C rules, which is a
+        # different handling than what our Preprocessor does out of the box.
+        # Hack around it enough that the configure tests work properly.
+        context = self.context
+        def normalize_numbers(value):
+            if isinstance(value, types.StringTypes):
+                if value[-1:] == 'L' and value[:-1].isdigit():
+                    value = int(value[:-1])
+            return value
+        self.context = self.Context(
+            (k, normalize_numbers(v)) for k, v in context.iteritems()
+        )
+        try:
+            return Preprocessor.do_if(self, *args, **kwargs)
+        finally:
+            self.context = context
+
+    class Context(dict):
+        def __missing__(self, key):
+            return None
+
+    def filter_c_substitution(self, line):
+        def repl(matchobj):
+            varname = matchobj.group('VAR')
+            if varname in self.context:
+                result = str(self.context[varname])
+                # The C preprocessor inserts whitespaces around expanded
+                # symbols.
+                start, end = matchobj.span('VAR')
+                if self.NON_WHITESPACE.match(line[start-1:start]):
+                    result = ' ' + result
+                if self.NON_WHITESPACE.match(line[end:end+1]):
+                    result = result + ' '
+                return result
+            return matchobj.group(0)
+        return self.VARSUBST.sub(repl, line)
+
+
+class TestCompilerPreprocessor(unittest.TestCase):
+    def test_expansion(self):
+        pp = CompilerPreprocessor({
+            'A': 1,
+            'B': '2',
+            'C': 'c',
+        })
+        pp.out = StringIO()
+        input = StringIO('A.B.C')
+        input.name = 'foo'
+        pp.do_include(input)
+
+        self.assertEquals(pp.out.getvalue(), '1 . 2 . c')
+
+    def test_condition(self):
+        pp = CompilerPreprocessor({
+            'A': 1,
+            'B': '2',
+            'C': '0L',
+        })
+        pp.out = StringIO()
+        input = StringIO(dedent('''\
+            #ifdef A
+            IFDEF_A
+            #endif
+            #if A
+            IF_A
+            #endif
+            #  if B
+            IF_B
+            #  else
+            IF_NOT_B
+            #  endif
+            #if !C
+            IF_NOT_C
+            #else
+            IF_C
+            #endif
+        '''))
+        input.name = 'foo'
+        pp.do_include(input)
+
+        self.assertEquals('IFDEF_A\nIF_A\nIF_B\nIF_NOT_C\n', pp.out.getvalue())
+
+
+class FakeCompiler(object):
+    '''Defines a fake compiler for use in toolchain tests below.
+
+    The definition given when creating an instance can have one of two
+    forms:
+    - a dict giving preprocessor symbols and their respective value, e.g.
+        { '__GNUC__': 4, '__STDC__': 1 }
+    - a dict associating flags to preprocessor symbols. An entry for `None`
+      is required in this case. Those are the baseline preprocessor symbols.
+      Additional entries describe additional flags to set or existing flags
+      to unset (with a value of `False`).
+        {
+          None: { '__GNUC__': 4, '__STDC__': 1, '__STRICT_ANSI__': 1 },
+          '-std=gnu99': { '__STDC_VERSION__': '199901L',
+                          '__STRICT_ANSI__': False },
+        }
+      With the dict above, invoking the preprocessor with no additional flags
+      would define __GNUC__, __STDC__ and __STRICT_ANSI__, and with -std=gnu99,
+      __GNUC__, __STDC__, and __STDC_VERSION__ (__STRICT_ANSI__ would be
+      unset).
+      It is also possible to have different symbols depending on the source
+      file extension. In this case, the key is '*.ext'. e.g.
+        {
+          '*.c': { '__STDC__': 1 },
+          '*.cpp': { '__cplusplus': '199711L' },
+        }
+    '''
+    def __init__(self, definition):
+        if definition.get(None) is None:
+            definition = {None: definition}
+        self._definition = definition
+
+    def __call__(self, stdin, args):
+        files = [arg for arg in args if not arg.startswith('-')]
+        flags = [arg for arg in args if arg.startswith('-')]
+        if '-E' in flags:
+            assert len(files) == 1
+            file = files[0]
+            pp = CompilerPreprocessor(self._definition[None])
+
+            def apply_defn(defn):
+                for k, v in defn.iteritems():
+                    if v is False:
+                        if k in pp.context:
+                            del pp.context[k]
+                    else:
+                        pp.context[k] = v
+
+            for glob, defn in self._definition.iteritems():
+                if glob and not glob.startswith('-') and fnmatch(file, glob):
+                    apply_defn(defn)
+
+            for flag in flags:
+                apply_defn(self._definition.get(flag, {}))
+
+            pp.out = StringIO()
+            pp.do_include(file)
+            return 0, pp.out.getvalue(), ''
+
+        return 1, '', ''
+
+
+class TestFakeCompiler(unittest.TestCase):
+    def test_fake_compiler(self):
+        with MockedOpen({
+            'file': 'A B C',
+            'file.c': 'A B C',
+        }):
+            compiler = FakeCompiler({
+                'A': '1',
+                'B': '2',
+            })
+            self.assertEquals(compiler(None, ['-E', 'file']),
+                              (0, '1 2 C', ''))
+
+            compiler = FakeCompiler({
+                None: {
+                    'A': '1',
+                    'B': '2',
+                },
+                '-foo': {
+                    'C': 'foo',
+                },
+                '-bar': {
+                    'B': 'bar',
+                    'C': 'bar',
+                },
+                '-qux': {
+                    'B': False,
+                },
+                '*.c': {
+                    'B': '42',
+                },
+            })
+            self.assertEquals(compiler(None, ['-E', 'file']),
+                              (0, '1 2 C', ''))
+            self.assertEquals(compiler(None, ['-E', '-foo', 'file']),
+                              (0, '1 2 foo', ''))
+            self.assertEquals(compiler(None, ['-E', '-bar', 'file']),
+                              (0, '1 bar bar', ''))
+            self.assertEquals(compiler(None, ['-E', '-qux', 'file']),
+                              (0, '1 B C', ''))
+            self.assertEquals(compiler(None, ['-E', '-foo', '-bar', 'file']),
+                              (0, '1 bar bar', ''))
+            self.assertEquals(compiler(None, ['-E', '-bar', '-foo', 'file']),
+                              (0, '1 bar foo', ''))
+            self.assertEquals(compiler(None, ['-E', '-bar', '-qux', 'file']),
+                              (0, '1 B bar', ''))
+            self.assertEquals(compiler(None, ['-E', '-qux', '-bar', 'file']),
+                              (0, '1 bar bar', ''))
+            self.assertEquals(compiler(None, ['-E', 'file.c']),
+                              (0, '1 42 C', ''))
+            self.assertEquals(compiler(None, ['-E', '-bar', 'file.c']),
+                              (0, '1 bar bar', ''))
+
+
+GCC_4_7 = FakeCompiler({
+    None: {
+        '__GNUC__': 4,
+        '__GNUC_MINOR__': 7,
+        '__GNUC_PATCHLEVEL__': 3,
+        '__STDC__': 1,
+    },
+    '-std=gnu99': {
+        '__STDC_VERSION__': '199901L',
+    },
+})
+
+GXX_4_7 = FakeCompiler({
+    None: {
+        '__GNUC__': 4,
+        '__GNUC_MINOR__': 7,
+        '__GNUC_PATCHLEVEL__': 3,
+        '__STDC__': 1,
+        '__cplusplus': '199711L',
+    },
+    '-std=gnu++11': {
+        '__cplusplus': '201103L',
+    },
+})
+
+GCC_4_9 = FakeCompiler({
+    None: {
+        '__GNUC__': 4,
+        '__GNUC_MINOR__': 9,
+        '__GNUC_PATCHLEVEL__': 3,
+        '__STDC__': 1,
+    },
+    '-std=gnu99': {
+        '__STDC_VERSION__': '199901L',
+    },
+})
+
+GXX_4_9 = FakeCompiler({
+    None: {
+        '__GNUC__': 4,
+        '__GNUC_MINOR__': 9,
+        '__GNUC_PATCHLEVEL__': 3,
+        '__STDC__': 1,
+        '__cplusplus': '199711L',
+    },
+    '-std=gnu++11': {
+        '__cplusplus': '201103L',
+    },
+})
+
+GCC_5 = FakeCompiler({
+    None: {
+        '__GNUC__': 5,
+        '__GNUC_MINOR__': 2,
+        '__GNUC_PATCHLEVEL__': 1,
+        '__STDC__': 1,
+        '__STDC_VERSION__': '201112L',
+    },
+    '-std=gnu99': {
+        '__STDC_VERSION__': '199901L',
+    },
+})
+
+GXX_5 = FakeCompiler({
+    None: {
+        '__GNUC__': 5,
+        '__GNUC_MINOR__': 2,
+        '__GNUC_PATCHLEVEL__': 1,
+        '__STDC__': 1,
+        '__cplusplus': '199711L',
+    },
+    '-std=gnu++11': {
+        '__cplusplus': '201103L',
+    },
+})
+
+CLANG_3_3 = FakeCompiler({
+    '__GNUC__': 4,
+    '__GNUC_MINOR__': 2,
+    '__GNUC_PATCHLEVEL__': 1,
+    '__clang__': 1,
+    '__clang_major__': 3,
+    '__clang_minor__': 3,
+    '__clang_patchlevel__': 0,
+    '__STDC__': 1,
+    '__STDC_VERSION__': '199901L',
+})
+
+CLANGXX_3_3 = FakeCompiler({
+    None: {
+        '__GNUC__': 4,
+        '__GNUC_MINOR__': 2,
+        '__GNUC_PATCHLEVEL__': 1,
+        '__clang__': 1,
+        '__clang_major__': 3,
+        '__clang_minor__': 3,
+        '__clang_patchlevel__': 0,
+        '__STDC__': 1,
+        '__cplusplus': '199711L',
+    },
+    '-std=gnu++11': {
+        '__cplusplus': '201103L',
+    },
+})
+
+CLANG_3_6 = FakeCompiler({
+    None: {
+        '__GNUC__': 4,
+        '__GNUC_MINOR__': 2,
+        '__GNUC_PATCHLEVEL__': 1,
+        '__clang__': 1,
+        '__clang_major__': 3,
+        '__clang_minor__': 6,
+        '__clang_patchlevel__': 2,
+        '__STDC__': 1,
+        '__STDC_VERSION__': '201112L',
+    },
+    '-std=gnu99': {
+        '__STDC_VERSION__': '199901L',
+    },
+})
+
+CLANGXX_3_6 = FakeCompiler({
+    None: {
+        '__GNUC__': 4,
+        '__GNUC_MINOR__': 2,
+        '__GNUC_PATCHLEVEL__': 1,
+        '__clang__': 1,
+        '__clang_major__': 3,
+        '__clang_minor__': 6,
+        '__clang_patchlevel__': 2,
+        '__STDC__': 1,
+        '__cplusplus': '199711L',
+    },
+    '-std=gnu++11': {
+        '__cplusplus': '201103L',
+        '__cpp_static_assert': '200410',
+    },
+})
+
+VS_2013u2 = FakeCompiler({
+    None: {
+        '_MSC_VER': '1800',
+        '_MSC_FULL_VER': '180030501',
+    },
+    '*.cpp': {
+        '__cplusplus': '199711L',
+    },
+})
+
+VS_2013u3 = FakeCompiler({
+    None: {
+        '_MSC_VER': '1800',
+        '_MSC_FULL_VER': '180030723',
+    },
+    '*.cpp': {
+        '__cplusplus': '199711L',
+    },
+})
+
+VS_2015 = FakeCompiler({
+    None: {
+        '_MSC_VER': '1900',
+        '_MSC_FULL_VER': '190023026',
+    },
+    '*.cpp': {
+        '__cplusplus': '199711L',
+    },
+})
+
+VS_2015u1 = FakeCompiler({
+    None: {
+        '_MSC_VER': '1900',
+        '_MSC_FULL_VER': '190023506',
+    },
+    '*.cpp': {
+        '__cplusplus': '199711L',
+    },
+})
+
+CLANG_CL = FakeCompiler({
+    None: {
+        '__clang__': 1,
+        '__clang_major__': 3,
+        '__clang_minor__': 9,
+        '__clang_patchlevel__': 0,
+        '__STDC_VERSION__': '201112L',
+        '_MSC_VER': '1800',
+        '_MSC_FULL_VER': '180000000',
+    },
+    '-std=gnu99': {  # In reality, the option needs to be preceded by -Xclang.
+        '__STDC_VERSION__': '199901L',
+    },
+    '*.cpp': {
+        '__STDC_VERSION__': False,
+        '__cplusplus': '201103L',
+    },
+    '-fms-compatibility-version=18.00.30723': {
+        '_MSC_FULL_VER': '180030723',
+    },
+})
+
+
+class BaseToolchainTest(BaseConfigureTest):
+    def setUp(self):
+        super(BaseToolchainTest, self).setUp()
+        self.out = StringIO()
+        self.logger = logging.getLogger('BaseToolchainTest')
+        self.logger.setLevel(logging.ERROR)
+        self.handler = logging.StreamHandler(self.out)
+        self.logger.addHandler(self.handler)
+
+    def tearDown(self):
+        self.logger.removeHandler(self.handler)
+        del self.handler
+        del self.out
+        super(BaseToolchainTest, self).tearDown()
+
+    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.
+          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'))
+
+        sandbox = self.get_sandbox(paths, {}, args, environ,
+                                   logger=self.logger)
+
+        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))
+            except SystemExit as e:
+                self.assertEquals((var, result),
+                                  (var, self.out.getvalue().strip()))
+                return
+
+
+class LinuxToolchainTest(BaseToolchainTest):
+    PATHS = {
+        '/usr/bin/gcc': GCC_4_9,
+        '/usr/bin/g++': GXX_4_9,
+        '/usr/bin/gcc-4.7': GCC_4_7,
+        '/usr/bin/g++-4.7': GXX_4_7,
+        '/usr/bin/gcc-5': GCC_5,
+        '/usr/bin/g++-5': GXX_5,
+        '/usr/bin/clang': CLANG_3_6,
+        '/usr/bin/clang++': CLANGXX_3_6,
+        '/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',
+    }
+    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++',
+    }
+
+    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,
+        })
+
+    def test_unsupported_gcc(self):
+        self.do_toolchain_test(self.PATHS, {
+            'c_compiler': self.GCC_4_7_RESULT,
+        }, environ={
+            'CC': 'gcc-4.7',
+            'CXX': 'g++-4.7',
+        })
+
+        # Maybe this should be reporting the mismatched version instead.
+        self.do_toolchain_test(self.PATHS, {
+            'c_compiler': self.GCC_4_9_RESULT,
+            'cxx_compiler': self.GXX_4_7_RESULT,
+        }, environ={
+            'CXX': 'g++-4.7',
+        })
+
+    def test_overridden_gcc(self):
+        self.do_toolchain_test(self.PATHS, {
+            'c_compiler': self.GCC_5_RESULT,
+            'cxx_compiler': self.GXX_5_RESULT,
+        }, environ={
+            'CC': 'gcc-5',
+            'CXX': 'g++-5',
+        })
+
+    def test_guess_cxx(self):
+        # When CXX is not set, we guess it from CC.
+        self.do_toolchain_test(self.PATHS, {
+            'c_compiler': self.GCC_5_RESULT,
+            'cxx_compiler': self.GXX_5_RESULT,
+        }, environ={
+            'CC': 'gcc-5',
+        })
+
+    def test_mismatched_gcc(self):
+        self.do_toolchain_test(self.PATHS, {
+            'c_compiler': self.GCC_4_9_RESULT,
+            'cxx_compiler': (
+                'The target C compiler is version 4.9.3, while the target '
+                'C++ compiler is version 5.2.1. Need to use the same compiler '
+                'version.'),
+        }, environ={
+            'CXX': 'g++-5',
+        })
+
+        self.do_toolchain_test(self.PATHS, {
+            'c_compiler': self.GCC_4_9_RESULT,
+            'cxx_compiler': self.GXX_4_9_RESULT,
+            'host_c_compiler': self.GCC_4_9_RESULT,
+            'host_cxx_compiler': (
+                'The host C compiler is version 4.9.3, while the host '
+                'C++ compiler is version 5.2.1. Need to use the same compiler '
+                'version.'),
+        }, environ={
+            'HOST_CXX': 'g++-5',
+        })
+
+    def test_mismatched_compiler(self):
+        self.do_toolchain_test(self.PATHS, {
+            'c_compiler': self.GCC_4_9_RESULT,
+            'cxx_compiler': (
+                'The target C compiler is gcc, while the target C++ compiler '
+                'is clang. Need to use the same compiler suite.'),
+        }, environ={
+            'CXX': 'clang++',
+        })
+
+        self.do_toolchain_test(self.PATHS, {
+            'c_compiler': self.GCC_4_9_RESULT,
+            'cxx_compiler': self.GXX_4_9_RESULT,
+            'host_c_compiler': self.GCC_4_9_RESULT,
+            'host_cxx_compiler': (
+                'The host C compiler is gcc, while the host C++ compiler '
+                'is clang. Need to use the same compiler suite.'),
+        }, environ={
+            'HOST_CXX': 'clang++',
+        })
+
+        self.do_toolchain_test(self.PATHS, {
+            'c_compiler': '`%s` is not a C compiler.'
+            % mozpath.abspath('/usr/bin/g++'),
+        }, environ={
+            'CC': 'g++',
+        })
+
+        self.do_toolchain_test(self.PATHS, {
+            'c_compiler': self.GCC_4_9_RESULT,
+            'cxx_compiler': '`%s` is not a C++ compiler.'
+            % mozpath.abspath('/usr/bin/gcc'),
+        }, environ={
+            'CXX': 'gcc',
+        })
+
+    def test_clang(self):
+        # We'll try gcc and clang, but since there is no gcc (gcc-x.y doesn't
+        # count), find clang.
+        paths = {
+            k: v for k, v in self.PATHS.iteritems()
+            if os.path.basename(k) not in ('gcc', 'g++')
+        }
+        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,
+        }, 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,
+            'cxx_compiler': self.CLANGXX_3_3_RESULT,
+        }, environ={
+            'CC': 'clang-3.3',
+            'CXX': 'clang++-3.3',
+        })
+
+    def test_no_supported_compiler(self):
+        # Even if there are gcc-x.y or clang-x.y compilers available, we
+        # don't try them. This could be considered something to improve.
+        paths = {
+            k: v for k, v in self.PATHS.iteritems()
+            if os.path.basename(k) not in ('gcc', 'g++', 'clang', 'clang++')
+        }
+        self.do_toolchain_test(paths, {
+            'c_compiler': 'Cannot find the target C compiler',
+        })
+
+    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,
+        }
+        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',
+        })
+
+    @unittest.expectedFailure  # Bug 1264609
+    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,
+        }, environ={
+            'CC': 'afl-clang-fast',
+            'CXX': 'afl-clang-fast++',
+        })
+
+
+class OSXToolchainTest(BaseToolchainTest):
+    HOST = 'x86_64-apple-darwin11.2.0'
+    PATHS = LinuxToolchainTest.PATHS
+    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_5_RESULT = LinuxToolchainTest.GCC_5_RESULT
+    GXX_5_RESULT = LinuxToolchainTest.GXX_5_RESULT
+
+    def test_clang(self):
+        # We only try clang because gcc is known not to work.
+        self.do_toolchain_test(self.PATHS, {
+            'c_compiler': self.CLANG_3_6_RESULT,
+            'cxx_compiler': self.CLANGXX_3_6_RESULT,
+        })
+
+    def test_not_gcc(self):
+        # We won't pick GCC if it's the only thing available.
+        paths = {
+            k: v for k, v in self.PATHS.iteritems()
+            if os.path.basename(k) not in ('clang', 'clang++')
+        }
+        self.do_toolchain_test(paths, {
+            'c_compiler': 'Cannot find the target C compiler',
+        })
+
+    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,
+            'cxx_compiler': self.CLANGXX_3_3_RESULT,
+        }, environ={
+            'CC': 'clang-3.3',
+            'CXX': 'clang++-3.3',
+        })
+
+    def test_forced_gcc(self):
+        # GCC can still be forced if the user really wants it.
+        self.do_toolchain_test(self.PATHS, {
+            'c_compiler': self.GCC_5_RESULT,
+            'cxx_compiler': self.GXX_5_RESULT,
+        }, environ={
+            'CC': 'gcc-5',
+            'CXX': 'g++-5',
+        })
+
+    def test_forced_unsupported_gcc(self):
+        self.do_toolchain_test(self.PATHS, {
+            'c_compiler': self.GCC_4_7_RESULT,
+        }, environ={
+            'CC': 'gcc-4.7',
+            'CXX': 'g++-4.7',
+        })
+
+
+class WindowsToolchainTest(BaseToolchainTest):
+    HOST = 'i686-pc-mingw32'
+
+    # For the purpose of this test, it doesn't matter that the paths are not
+    # real Windows paths.
+    PATHS = {
+        '/opt/VS_2013u2/bin/cl': VS_2013u2,
+        '/opt/VS_2013u3/bin/cl': VS_2013u3,
+        '/opt/VS_2015/bin/cl': VS_2015,
+        '/usr/bin/cl': VS_2015u1,
+        '/usr/bin/clang-cl': CLANG_CL,
+    }
+    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_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_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_RESULT = {
+        '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
+    GXX_5_RESULT = LinuxToolchainTest.GXX_5_RESULT
+
+    def test_msvc(self):
+        self.do_toolchain_test(self.PATHS, {
+            'c_compiler': self.VS_2015u1_RESULT,
+            'cxx_compiler': self.VS_2015u1_RESULT,
+        })
+
+    def test_msvc_2013(self):
+        self.do_toolchain_test(self.PATHS, {
+            'c_compiler': self.VS_2013u3_RESULT,
+            'cxx_compiler': self.VS_2013u3_RESULT,
+        }, environ={
+            'CC': '/opt/VS_2013u3/bin/cl',
+        })
+
+    def test_unsupported_msvc(self):
+        # While 2013 is supported, update 3 or higher is needed.
+        self.do_toolchain_test(self.PATHS, {
+            'c_compiler': self.VS_2013u2_RESULT,
+        }, environ={
+            'CC': '/opt/VS_2013u2/bin/cl',
+        })
+
+        # Likewise with 2015, update 1 or higher is needed.
+        self.do_toolchain_test(self.PATHS, {
+            'c_compiler': self.VS_2015_RESULT,
+        }, environ={
+            'CC': '/opt/VS_2015/bin/cl',
+        })
+
+    def test_clang_cl(self):
+        # We'll pick clang-cl if msvc can't be found.
+        paths = {
+            k: v for k, v in self.PATHS.iteritems()
+            if os.path.basename(k) != 'cl'
+        }
+        self.do_toolchain_test(paths, {
+            'c_compiler': self.CLANG_CL_RESULT,
+            'cxx_compiler': self.CLANGXX_CL_RESULT,
+        })
+
+    def test_gcc(self):
+        # We'll pick GCC if msvc and clang-cl can't be found.
+        paths = {
+            k: v for k, v in self.PATHS.iteritems()
+            if os.path.basename(k) not in ('cl', 'clang-cl')
+        }
+        self.do_toolchain_test(paths, {
+            'c_compiler': self.GCC_4_9_RESULT,
+            'cxx_compiler': self.GXX_4_9_RESULT,
+        })
+
+    def test_overridden_unsupported_gcc(self):
+        self.do_toolchain_test(self.PATHS, {
+            'c_compiler': self.GCC_4_7_RESULT,
+        }, environ={
+            'CC': 'gcc-4.7',
+            'CXX': 'g++-4.7',
+        })
+
+    def test_clang(self):
+        # We'll pick clang if nothing else is found.
+        paths = {
+            k: v for k, v in self.PATHS.iteritems()
+            if os.path.basename(k) not in ('cl', 'clang-cl', 'gcc')
+        }
+        self.do_toolchain_test(paths, {
+            'c_compiler': self.CLANG_3_6_RESULT,
+            'cxx_compiler': self.CLANGXX_3_6_RESULT,
+        })
+
+    def test_overridden_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,
+            'cxx_compiler': self.CLANGXX_3_3_RESULT,
+        }, environ={
+            'CC': 'clang-3.3',
+            'CXX': 'clang++-3.3',
+        })
+
+
+class CrossCompileToolchainTest(BaseToolchainTest):
+    PATHS = {
+        '/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'
+    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,
+            '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,
+            'host_c_compiler': self.GCC_4_9_RESULT,
+            'host_cxx_compiler': self.GXX_4_9_RESULT,
+        }, args=['--target=arm-unknown-linux-gnu'], environ={
+            'CC': 'arm-linux-gnu-gcc-5',
+            'CXX': 'arm-linux-gnu-g++-5',
+        })
+
+    def test_overridden_unsupported_cross_gcc(self):
+        self.do_toolchain_test(self.PATHS, {
+            'c_compiler': self.ARM_GCC_4_7_RESULT,
+        }, args=['--target=arm-unknown-linux-gnu'], environ={
+            'CC': 'arm-linux-gnu-gcc-4.7',
+            'CXX': 'arm-linux-gnu-g++-4.7',
+        })
+
+    def test_guess_cross_cxx(self):
+        # When CXX is not set, we guess it from CC.
+        self.do_toolchain_test(self.PATHS, {
+            'c_compiler': self.ARM_GCC_5_RESULT,
+            'cxx_compiler': self.ARM_GXX_5_RESULT,
+            'host_c_compiler': self.GCC_4_9_RESULT,
+            'host_cxx_compiler': self.GXX_4_9_RESULT,
+        }, args=['--target=arm-unknown-linux-gnu'], environ={
+            'CC': 'arm-linux-gnu-gcc-5',
+        })
+
+        self.do_toolchain_test(self.PATHS, {
+            'c_compiler': self.ARM_GCC_5_RESULT,
+            'cxx_compiler': self.ARM_GXX_5_RESULT,
+            'host_c_compiler': self.CLANG_3_6_RESULT,
+            'host_cxx_compiler': self.CLANGXX_3_6_RESULT,
+        }, args=['--target=arm-unknown-linux-gnu'], environ={
+            'CC': 'arm-linux-gnu-gcc-5',
+            'HOST_CC': 'clang',
+        })
+
+
+if __name__ == '__main__':
+    main()