Home | History | Annotate | Download | only in lib
      1 # Copyright (c) 2012 Google Inc. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """
      6 TestGyp.py:  a testing framework for GYP integration tests.
      7 """
      8 
      9 import collections
     10 from contextlib import contextmanager
     11 import itertools
     12 import os
     13 import re
     14 import shutil
     15 import subprocess
     16 import sys
     17 import tempfile
     18 
     19 import TestCmd
     20 import TestCommon
     21 from TestCommon import __all__
     22 
     23 __all__.extend([
     24   'TestGyp',
     25 ])
     26 
     27 
     28 def remove_debug_line_numbers(contents):
     29   """Function to remove the line numbers from the debug output
     30   of gyp and thus reduce the extreme fragility of the stdout
     31   comparison tests.
     32   """
     33   lines = contents.splitlines()
     34   # split each line on ":"
     35   lines = [l.split(":", 3) for l in lines]
     36   # join each line back together while ignoring the
     37   # 3rd column which is the line number
     38   lines = [len(l) > 3 and ":".join(l[3:]) or l for l in lines]
     39   return "\n".join(lines)
     40 
     41 
     42 def match_modulo_line_numbers(contents_a, contents_b):
     43   """File contents matcher that ignores line numbers."""
     44   contents_a = remove_debug_line_numbers(contents_a)
     45   contents_b = remove_debug_line_numbers(contents_b)
     46   return TestCommon.match_exact(contents_a, contents_b)
     47 
     48 
     49 @contextmanager
     50 def LocalEnv(local_env):
     51   """Context manager to provide a local OS environment."""
     52   old_env = os.environ.copy()
     53   os.environ.update(local_env)
     54   try:
     55     yield
     56   finally:
     57     os.environ.clear()
     58     os.environ.update(old_env)
     59 
     60 
     61 class TestGypBase(TestCommon.TestCommon):
     62   """
     63   Class for controlling end-to-end tests of gyp generators.
     64 
     65   Instantiating this class will create a temporary directory and
     66   arrange for its destruction (via the TestCmd superclass) and
     67   copy all of the non-gyptest files in the directory hierarchy of the
     68   executing script.
     69 
     70   The default behavior is to test the 'gyp' or 'gyp.bat' file in the
     71   current directory.  An alternative may be specified explicitly on
     72   instantiation, or by setting the TESTGYP_GYP environment variable.
     73 
     74   This class should be subclassed for each supported gyp generator
     75   (format).  Various abstract methods below define calling signatures
     76   used by the test scripts to invoke builds on the generated build
     77   configuration and to run executables generated by those builds.
     78   """
     79 
     80   formats = []
     81   build_tool = None
     82   build_tool_list = []
     83 
     84   _exe = TestCommon.exe_suffix
     85   _obj = TestCommon.obj_suffix
     86   shobj_ = TestCommon.shobj_prefix
     87   _shobj = TestCommon.shobj_suffix
     88   lib_ = TestCommon.lib_prefix
     89   _lib = TestCommon.lib_suffix
     90   dll_ = TestCommon.dll_prefix
     91   _dll = TestCommon.dll_suffix
     92   module_ = TestCommon.module_prefix
     93   _module = TestCommon.module_suffix
     94 
     95   # Constants to represent different targets.
     96   ALL = '__all__'
     97   DEFAULT = '__default__'
     98 
     99   # Constants for different target types.
    100   EXECUTABLE = '__executable__'
    101   STATIC_LIB = '__static_lib__'
    102   SHARED_LIB = '__shared_lib__'
    103   LOADABLE_MODULE = '__loadable_module__'
    104 
    105   def __init__(self, gyp=None, *args, **kw):
    106     self.origin_cwd = os.path.abspath(os.path.dirname(sys.argv[0]))
    107     self.extra_args = sys.argv[1:]
    108 
    109     if not gyp:
    110       gyp = os.environ.get('TESTGYP_GYP')
    111       if not gyp:
    112         if sys.platform == 'win32':
    113           gyp = 'gyp.bat'
    114         else:
    115           gyp = 'gyp'
    116     self.gyp = os.path.abspath(gyp)
    117     self.no_parallel = False
    118 
    119     self.formats = [self.format]
    120 
    121     self.initialize_build_tool()
    122 
    123     kw.setdefault('match', TestCommon.match_exact)
    124 
    125     # Put test output in out/testworkarea by default.
    126     # Use temporary names so there are no collisions.
    127     workdir = os.path.join('out', kw.get('workdir', 'testworkarea'))
    128     # Create work area if it doesn't already exist.
    129     if not os.path.isdir(workdir):
    130       os.makedirs(workdir)
    131 
    132     kw['workdir'] = tempfile.mktemp(prefix='testgyp.', dir=workdir)
    133 
    134     formats = kw.pop('formats', [])
    135 
    136     super(TestGypBase, self).__init__(*args, **kw)
    137 
    138     real_format = self.format.split('-')[-1]
    139     excluded_formats = set([f for f in formats if f[0] == '!'])
    140     included_formats = set(formats) - excluded_formats
    141     if ('!'+real_format in excluded_formats or
    142         included_formats and real_format not in included_formats):
    143       msg = 'Invalid test for %r format; skipping test.\n'
    144       self.skip_test(msg % self.format)
    145 
    146     self.copy_test_configuration(self.origin_cwd, self.workdir)
    147     self.set_configuration(None)
    148 
    149     # Set $HOME so that gyp doesn't read the user's actual
    150     # ~/.gyp/include.gypi file, which may contain variables
    151     # and other settings that would change the output.
    152     os.environ['HOME'] = self.workpath()
    153     # Clear $GYP_DEFINES for the same reason.
    154     if 'GYP_DEFINES' in os.environ:
    155       del os.environ['GYP_DEFINES']
    156     # Override the user's language settings, which could
    157     # otherwise make the output vary from what is expected.
    158     os.environ['LC_ALL'] = 'C'
    159 
    160   def built_file_must_exist(self, name, type=None, **kw):
    161     """
    162     Fails the test if the specified built file name does not exist.
    163     """
    164     return self.must_exist(self.built_file_path(name, type, **kw))
    165 
    166   def built_file_must_not_exist(self, name, type=None, **kw):
    167     """
    168     Fails the test if the specified built file name exists.
    169     """
    170     return self.must_not_exist(self.built_file_path(name, type, **kw))
    171 
    172   def built_file_must_match(self, name, contents, **kw):
    173     """
    174     Fails the test if the contents of the specified built file name
    175     do not match the specified contents.
    176     """
    177     return self.must_match(self.built_file_path(name, **kw), contents)
    178 
    179   def built_file_must_not_match(self, name, contents, **kw):
    180     """
    181     Fails the test if the contents of the specified built file name
    182     match the specified contents.
    183     """
    184     return self.must_not_match(self.built_file_path(name, **kw), contents)
    185 
    186   def built_file_must_not_contain(self, name, contents, **kw):
    187     """
    188     Fails the test if the specified built file name contains the specified
    189     contents.
    190     """
    191     return self.must_not_contain(self.built_file_path(name, **kw), contents)
    192 
    193   def copy_test_configuration(self, source_dir, dest_dir):
    194     """
    195     Copies the test configuration from the specified source_dir
    196     (the directory in which the test script lives) to the
    197     specified dest_dir (a temporary working directory).
    198 
    199     This ignores all files and directories that begin with
    200     the string 'gyptest', and all '.svn' subdirectories.
    201     """
    202     for root, dirs, files in os.walk(source_dir):
    203       if '.svn' in dirs:
    204         dirs.remove('.svn')
    205       dirs = [ d for d in dirs if not d.startswith('gyptest') ]
    206       files = [ f for f in files if not f.startswith('gyptest') ]
    207       for dirname in dirs:
    208         source = os.path.join(root, dirname)
    209         destination = source.replace(source_dir, dest_dir)
    210         os.mkdir(destination)
    211         if sys.platform != 'win32':
    212           shutil.copystat(source, destination)
    213       for filename in files:
    214         source = os.path.join(root, filename)
    215         destination = source.replace(source_dir, dest_dir)
    216         shutil.copy2(source, destination)
    217 
    218     # The gyp tests are run with HOME pointing to |dest_dir| to provide an
    219     # hermetic environment. Symlink login.keychain and the 'Provisioning
    220     # Profiles' folder to allow codesign to access to the data required for
    221     # signing binaries.
    222     if sys.platform == 'darwin':
    223       old_keychain = GetDefaultKeychainPath()
    224       old_provisioning_profiles = os.path.join(
    225           os.environ['HOME'], 'Library', 'MobileDevice',
    226           'Provisioning Profiles')
    227 
    228       new_keychain = os.path.join(dest_dir, 'Library', 'Keychains')
    229       MakeDirs(new_keychain)
    230       os.symlink(old_keychain, os.path.join(new_keychain, 'login.keychain'))
    231 
    232       if os.path.exists(old_provisioning_profiles):
    233         new_provisioning_profiles = os.path.join(
    234             dest_dir, 'Library', 'MobileDevice')
    235         MakeDirs(new_provisioning_profiles)
    236         os.symlink(old_provisioning_profiles,
    237             os.path.join(new_provisioning_profiles, 'Provisioning Profiles'))
    238 
    239   def initialize_build_tool(self):
    240     """
    241     Initializes the .build_tool attribute.
    242 
    243     Searches the .build_tool_list for an executable name on the user's
    244     $PATH.  The first tool on the list is used as-is if nothing is found
    245     on the current $PATH.
    246     """
    247     for build_tool in self.build_tool_list:
    248       if not build_tool:
    249         continue
    250       if os.path.isabs(build_tool):
    251         self.build_tool = build_tool
    252         return
    253       build_tool = self.where_is(build_tool)
    254       if build_tool:
    255         self.build_tool = build_tool
    256         return
    257 
    258     if self.build_tool_list:
    259       self.build_tool = self.build_tool_list[0]
    260 
    261   def relocate(self, source, destination):
    262     """
    263     Renames (relocates) the specified source (usually a directory)
    264     to the specified destination, creating the destination directory
    265     first if necessary.
    266 
    267     Note:  Don't use this as a generic "rename" operation.  In the
    268     future, "relocating" parts of a GYP tree may affect the state of
    269     the test to modify the behavior of later method calls.
    270     """
    271     destination_dir = os.path.dirname(destination)
    272     if not os.path.exists(destination_dir):
    273       self.subdir(destination_dir)
    274     os.rename(source, destination)
    275 
    276   def report_not_up_to_date(self):
    277     """
    278     Reports that a build is not up-to-date.
    279 
    280     This provides common reporting for formats that have complicated
    281     conditions for checking whether a build is up-to-date.  Formats
    282     that expect exact output from the command (make) can
    283     just set stdout= when they call the run_build() method.
    284     """
    285     print "Build is not up-to-date:"
    286     print self.banner('STDOUT ')
    287     print self.stdout()
    288     stderr = self.stderr()
    289     if stderr:
    290       print self.banner('STDERR ')
    291       print stderr
    292 
    293   def run_gyp(self, gyp_file, *args, **kw):
    294     """
    295     Runs gyp against the specified gyp_file with the specified args.
    296     """
    297 
    298     # When running gyp, and comparing its output we use a comparitor
    299     # that ignores the line numbers that gyp logs in its debug output.
    300     if kw.pop('ignore_line_numbers', False):
    301       kw.setdefault('match', match_modulo_line_numbers)
    302 
    303     # TODO:  --depth=. works around Chromium-specific tree climbing.
    304     depth = kw.pop('depth', '.')
    305     run_args = ['--depth='+depth]
    306     run_args.extend(['--format='+f for f in self.formats]);
    307     run_args.append(gyp_file)
    308     if self.no_parallel:
    309       run_args += ['--no-parallel']
    310     # TODO: if extra_args contains a '--build' flag
    311     # we really want that to only apply to the last format (self.format).
    312     run_args.extend(self.extra_args)
    313     # Default xcode_ninja_target_pattern to ^.*$ to fix xcode-ninja tests
    314     xcode_ninja_target_pattern = kw.pop('xcode_ninja_target_pattern', '.*')
    315     run_args.extend(
    316       ['-G', 'xcode_ninja_target_pattern=%s' % xcode_ninja_target_pattern])
    317     run_args.extend(args)
    318     return self.run(program=self.gyp, arguments=run_args, **kw)
    319 
    320   def run(self, *args, **kw):
    321     """
    322     Executes a program by calling the superclass .run() method.
    323 
    324     This exists to provide a common place to filter out keyword
    325     arguments implemented in this layer, without having to update
    326     the tool-specific subclasses or clutter the tests themselves
    327     with platform-specific code.
    328     """
    329     if kw.has_key('SYMROOT'):
    330       del kw['SYMROOT']
    331     super(TestGypBase, self).run(*args, **kw)
    332 
    333   def set_configuration(self, configuration):
    334     """
    335     Sets the configuration, to be used for invoking the build
    336     tool and testing potential built output.
    337     """
    338     self.configuration = configuration
    339 
    340   def configuration_dirname(self):
    341     if self.configuration:
    342       return self.configuration.split('|')[0]
    343     else:
    344       return 'Default'
    345 
    346   def configuration_buildname(self):
    347     if self.configuration:
    348       return self.configuration
    349     else:
    350       return 'Default'
    351 
    352   #
    353   # Abstract methods to be defined by format-specific subclasses.
    354   #
    355 
    356   def build(self, gyp_file, target=None, **kw):
    357     """
    358     Runs a build of the specified target against the configuration
    359     generated from the specified gyp_file.
    360 
    361     A 'target' argument of None or the special value TestGyp.DEFAULT
    362     specifies the default argument for the underlying build tool.
    363     A 'target' argument of TestGyp.ALL specifies the 'all' target
    364     (if any) of the underlying build tool.
    365     """
    366     raise NotImplementedError
    367 
    368   def built_file_path(self, name, type=None, **kw):
    369     """
    370     Returns a path to the specified file name, of the specified type.
    371     """
    372     raise NotImplementedError
    373 
    374   def built_file_basename(self, name, type=None, **kw):
    375     """
    376     Returns the base name of the specified file name, of the specified type.
    377 
    378     A bare=True keyword argument specifies that prefixes and suffixes shouldn't
    379     be applied.
    380     """
    381     if not kw.get('bare'):
    382       if type == self.EXECUTABLE:
    383         name = name + self._exe
    384       elif type == self.STATIC_LIB:
    385         name = self.lib_ + name + self._lib
    386       elif type == self.SHARED_LIB:
    387         name = self.dll_ + name + self._dll
    388       elif type == self.LOADABLE_MODULE:
    389         name = self.module_ + name + self._module
    390     return name
    391 
    392   def run_built_executable(self, name, *args, **kw):
    393     """
    394     Runs an executable program built from a gyp-generated configuration.
    395 
    396     The specified name should be independent of any particular generator.
    397     Subclasses should find the output executable in the appropriate
    398     output build directory, tack on any necessary executable suffix, etc.
    399     """
    400     raise NotImplementedError
    401 
    402   def up_to_date(self, gyp_file, target=None, **kw):
    403     """
    404     Verifies that a build of the specified target is up to date.
    405 
    406     The subclass should implement this by calling build()
    407     (or a reasonable equivalent), checking whatever conditions
    408     will tell it the build was an "up to date" null build, and
    409     failing if it isn't.
    410     """
    411     raise NotImplementedError
    412 
    413 
    414 class TestGypGypd(TestGypBase):
    415   """
    416   Subclass for testing the GYP 'gypd' generator (spit out the
    417   internal data structure as pretty-printed Python).
    418   """
    419   format = 'gypd'
    420   def __init__(self, gyp=None, *args, **kw):
    421     super(TestGypGypd, self).__init__(*args, **kw)
    422     # gypd implies the use of 'golden' files, so parallelizing conflicts as it
    423     # causes ordering changes.
    424     self.no_parallel = True
    425 
    426 
    427 class TestGypCustom(TestGypBase):
    428   """
    429   Subclass for testing the GYP with custom generator
    430   """
    431 
    432   def __init__(self, gyp=None, *args, **kw):
    433     self.format = kw.pop("format")
    434     super(TestGypCustom, self).__init__(*args, **kw)
    435 
    436 
    437 class TestGypCMake(TestGypBase):
    438   """
    439   Subclass for testing the GYP CMake generator, using cmake's ninja backend.
    440   """
    441   format = 'cmake'
    442   build_tool_list = ['cmake']
    443   ALL = 'all'
    444 
    445   def cmake_build(self, gyp_file, target=None, **kw):
    446     arguments = kw.get('arguments', [])[:]
    447 
    448     self.build_tool_list = ['cmake']
    449     self.initialize_build_tool()
    450 
    451     chdir = os.path.join(kw.get('chdir', '.'),
    452                          'out',
    453                          self.configuration_dirname())
    454     kw['chdir'] = chdir
    455 
    456     arguments.append('-G')
    457     arguments.append('Ninja')
    458 
    459     kw['arguments'] = arguments
    460 
    461     stderr = kw.get('stderr', None)
    462     if stderr:
    463       kw['stderr'] = stderr.split('$$$')[0]
    464 
    465     self.run(program=self.build_tool, **kw)
    466 
    467   def ninja_build(self, gyp_file, target=None, **kw):
    468     arguments = kw.get('arguments', [])[:]
    469 
    470     self.build_tool_list = ['ninja']
    471     self.initialize_build_tool()
    472 
    473     # Add a -C output/path to the command line.
    474     arguments.append('-C')
    475     arguments.append(os.path.join('out', self.configuration_dirname()))
    476 
    477     if target not in (None, self.DEFAULT):
    478       arguments.append(target)
    479 
    480     kw['arguments'] = arguments
    481 
    482     stderr = kw.get('stderr', None)
    483     if stderr:
    484       stderrs = stderr.split('$$$')
    485       kw['stderr'] = stderrs[1] if len(stderrs) > 1 else ''
    486 
    487     return self.run(program=self.build_tool, **kw)
    488 
    489   def build(self, gyp_file, target=None, status=0, **kw):
    490     # Two tools must be run to build, cmake and the ninja.
    491     # Allow cmake to succeed when the overall expectation is to fail.
    492     if status is None:
    493       kw['status'] = None
    494     else:
    495       if not isinstance(status, collections.Iterable): status = (status,)
    496       kw['status'] = list(itertools.chain((0,), status))
    497     self.cmake_build(gyp_file, target, **kw)
    498     kw['status'] = status
    499     self.ninja_build(gyp_file, target, **kw)
    500 
    501   def run_built_executable(self, name, *args, **kw):
    502     # Enclosing the name in a list avoids prepending the original dir.
    503     program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
    504     if sys.platform == 'darwin':
    505       configuration = self.configuration_dirname()
    506       os.environ['DYLD_LIBRARY_PATH'] = os.path.join('out', configuration)
    507     return self.run(program=program, *args, **kw)
    508 
    509   def built_file_path(self, name, type=None, **kw):
    510     result = []
    511     chdir = kw.get('chdir')
    512     if chdir:
    513       result.append(chdir)
    514     result.append('out')
    515     result.append(self.configuration_dirname())
    516     if type == self.STATIC_LIB:
    517       if sys.platform != 'darwin':
    518         result.append('obj.target')
    519     elif type == self.SHARED_LIB:
    520       if sys.platform != 'darwin' and sys.platform != 'win32':
    521         result.append('lib.target')
    522     subdir = kw.get('subdir')
    523     if subdir and type != self.SHARED_LIB:
    524       result.append(subdir)
    525     result.append(self.built_file_basename(name, type, **kw))
    526     return self.workpath(*result)
    527 
    528   def up_to_date(self, gyp_file, target=None, **kw):
    529     result = self.ninja_build(gyp_file, target, **kw)
    530     if not result:
    531       stdout = self.stdout()
    532       if 'ninja: no work to do' not in stdout:
    533         self.report_not_up_to_date()
    534         self.fail_test()
    535     return result
    536 
    537 
    538 class TestGypMake(TestGypBase):
    539   """
    540   Subclass for testing the GYP Make generator.
    541   """
    542   format = 'make'
    543   build_tool_list = ['make']
    544   ALL = 'all'
    545   def build(self, gyp_file, target=None, **kw):
    546     """
    547     Runs a Make build using the Makefiles generated from the specified
    548     gyp_file.
    549     """
    550     arguments = kw.get('arguments', [])[:]
    551     if self.configuration:
    552       arguments.append('BUILDTYPE=' + self.configuration)
    553     if target not in (None, self.DEFAULT):
    554       arguments.append(target)
    555     # Sub-directory builds provide per-gyp Makefiles (i.e.
    556     # Makefile.gyp_filename), so use that if there is no Makefile.
    557     chdir = kw.get('chdir', '')
    558     if not os.path.exists(os.path.join(chdir, 'Makefile')):
    559       print "NO Makefile in " + os.path.join(chdir, 'Makefile')
    560       arguments.insert(0, '-f')
    561       arguments.insert(1, os.path.splitext(gyp_file)[0] + '.Makefile')
    562     kw['arguments'] = arguments
    563     return self.run(program=self.build_tool, **kw)
    564   def up_to_date(self, gyp_file, target=None, **kw):
    565     """
    566     Verifies that a build of the specified Make target is up to date.
    567     """
    568     if target in (None, self.DEFAULT):
    569       message_target = 'all'
    570     else:
    571       message_target = target
    572     kw['stdout'] = "make: Nothing to be done for `%s'.\n" % message_target
    573     return self.build(gyp_file, target, **kw)
    574   def run_built_executable(self, name, *args, **kw):
    575     """
    576     Runs an executable built by Make.
    577     """
    578     configuration = self.configuration_dirname()
    579     libdir = os.path.join('out', configuration, 'lib')
    580     # TODO(piman): when everything is cross-compile safe, remove lib.target
    581     if sys.platform == 'darwin':
    582       # Mac puts target shared libraries right in the product directory.
    583       configuration = self.configuration_dirname()
    584       os.environ['DYLD_LIBRARY_PATH'] = (
    585           libdir + '.host:' + os.path.join('out', configuration))
    586     else:
    587       os.environ['LD_LIBRARY_PATH'] = libdir + '.host:' + libdir + '.target'
    588     # Enclosing the name in a list avoids prepending the original dir.
    589     program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
    590     return self.run(program=program, *args, **kw)
    591   def built_file_path(self, name, type=None, **kw):
    592     """
    593     Returns a path to the specified file name, of the specified type,
    594     as built by Make.
    595 
    596     Built files are in the subdirectory 'out/{configuration}'.
    597     The default is 'out/Default'.
    598 
    599     A chdir= keyword argument specifies the source directory
    600     relative to which  the output subdirectory can be found.
    601 
    602     "type" values of STATIC_LIB or SHARED_LIB append the necessary
    603     prefixes and suffixes to a platform-independent library base name.
    604 
    605     A subdir= keyword argument specifies a library subdirectory within
    606     the default 'obj.target'.
    607     """
    608     result = []
    609     chdir = kw.get('chdir')
    610     if chdir:
    611       result.append(chdir)
    612     configuration = self.configuration_dirname()
    613     result.extend(['out', configuration])
    614     if type == self.STATIC_LIB and sys.platform != 'darwin':
    615       result.append('obj.target')
    616     elif type == self.SHARED_LIB and sys.platform != 'darwin':
    617       result.append('lib.target')
    618     subdir = kw.get('subdir')
    619     if subdir and type != self.SHARED_LIB:
    620       result.append(subdir)
    621     result.append(self.built_file_basename(name, type, **kw))
    622     return self.workpath(*result)
    623 
    624 
    625 def ConvertToCygpath(path):
    626   """Convert to cygwin path if we are using cygwin."""
    627   if sys.platform == 'cygwin':
    628     p = subprocess.Popen(['cygpath', path], stdout=subprocess.PIPE)
    629     path = p.communicate()[0].strip()
    630   return path
    631 
    632 
    633 def MakeDirs(new_dir):
    634   """A wrapper around os.makedirs() that emulates "mkdir -p"."""
    635   try:
    636     os.makedirs(new_dir)
    637   except OSError as e:
    638     if e.errno != errno.EEXIST:
    639       raise
    640 
    641 def GetDefaultKeychainPath():
    642   """Get the keychain path, for used before updating HOME."""
    643   assert sys.platform == 'darwin'
    644   # Format is:
    645   # $ security default-keychain
    646   #     "/Some/Path/To/default.keychain"
    647   path = subprocess.check_output(['security', 'default-keychain']).strip()
    648   return path[1:-1]
    649 
    650 def FindMSBuildInstallation(msvs_version = 'auto'):
    651   """Returns path to MSBuild for msvs_version or latest available.
    652 
    653   Looks in the registry to find install location of MSBuild.
    654   MSBuild before v4.0 will not build c++ projects, so only use newer versions.
    655   """
    656   import TestWin
    657   registry = TestWin.Registry()
    658 
    659   msvs_to_msbuild = {
    660       '2013': r'12.0',
    661       '2012': r'4.0',  # Really v4.0.30319 which comes with .NET 4.5.
    662       '2010': r'4.0'}
    663 
    664   msbuild_basekey = r'HKLM\SOFTWARE\Microsoft\MSBuild\ToolsVersions'
    665   if not registry.KeyExists(msbuild_basekey):
    666     print 'Error: could not find MSBuild base registry entry'
    667     return None
    668 
    669   msbuild_version = None
    670   if msvs_version in msvs_to_msbuild:
    671     msbuild_test_version = msvs_to_msbuild[msvs_version]
    672     if registry.KeyExists(msbuild_basekey + '\\' + msbuild_test_version):
    673       msbuild_version = msbuild_test_version
    674     else:
    675       print ('Warning: Environment variable GYP_MSVS_VERSION specifies "%s" '
    676              'but corresponding MSBuild "%s" was not found.' %
    677              (msvs_version, msbuild_version))
    678   if not msbuild_version:
    679     for msvs_version in sorted(msvs_to_msbuild, reverse=True):
    680       msbuild_test_version = msvs_to_msbuild[msvs_version]
    681       if registry.KeyExists(msbuild_basekey + '\\' + msbuild_test_version):
    682         msbuild_version = msbuild_test_version
    683         break
    684   if not msbuild_version:
    685     print 'Error: could not find MSBuild registry entry'
    686     return None
    687 
    688   msbuild_path = registry.GetValue(msbuild_basekey + '\\' + msbuild_version,
    689                                    'MSBuildToolsPath')
    690   if not msbuild_path:
    691     print 'Error: could not get MSBuild registry entry value'
    692     return None
    693 
    694   return os.path.join(msbuild_path, 'MSBuild.exe')
    695 
    696 
    697 def FindVisualStudioInstallation():
    698   """Returns appropriate values for .build_tool and .uses_msbuild fields
    699   of TestGypBase for Visual Studio.
    700 
    701   We use the value specified by GYP_MSVS_VERSION.  If not specified, we
    702   search %PATH% and %PATHEXT% for a devenv.{exe,bat,...} executable.
    703   Failing that, we search for likely deployment paths.
    704   """
    705   possible_roots = ['%s:\\Program Files%s' % (chr(drive), suffix)
    706                     for drive in range(ord('C'), ord('Z') + 1)
    707                     for suffix in ['', ' (x86)']]
    708   possible_paths = {
    709       '2015': r'Microsoft Visual Studio 14.0\Common7\IDE\devenv.com',
    710       '2013': r'Microsoft Visual Studio 12.0\Common7\IDE\devenv.com',
    711       '2012': r'Microsoft Visual Studio 11.0\Common7\IDE\devenv.com',
    712       '2010': r'Microsoft Visual Studio 10.0\Common7\IDE\devenv.com',
    713       '2008': r'Microsoft Visual Studio 9.0\Common7\IDE\devenv.com',
    714       '2005': r'Microsoft Visual Studio 8\Common7\IDE\devenv.com'}
    715 
    716   possible_roots = [ConvertToCygpath(r) for r in possible_roots]
    717 
    718   msvs_version = 'auto'
    719   for flag in (f for f in sys.argv if f.startswith('msvs_version=')):
    720     msvs_version = flag.split('=')[-1]
    721   msvs_version = os.environ.get('GYP_MSVS_VERSION', msvs_version)
    722 
    723   if msvs_version in possible_paths:
    724     # Check that the path to the specified GYP_MSVS_VERSION exists.
    725     path = possible_paths[msvs_version]
    726     for r in possible_roots:
    727       build_tool = os.path.join(r, path)
    728       if os.path.exists(build_tool):
    729         uses_msbuild = msvs_version >= '2010'
    730         msbuild_path = FindMSBuildInstallation(msvs_version)
    731         return build_tool, uses_msbuild, msbuild_path
    732     else:
    733       print ('Warning: Environment variable GYP_MSVS_VERSION specifies "%s" '
    734               'but corresponding "%s" was not found.' % (msvs_version, path))
    735   # Neither GYP_MSVS_VERSION nor the path help us out.  Iterate through
    736   # the choices looking for a match.
    737   for version in sorted(possible_paths, reverse=True):
    738     path = possible_paths[version]
    739     for r in possible_roots:
    740       build_tool = os.path.join(r, path)
    741       if os.path.exists(build_tool):
    742         uses_msbuild = msvs_version >= '2010'
    743         msbuild_path = FindMSBuildInstallation(msvs_version)
    744         return build_tool, uses_msbuild, msbuild_path
    745   print 'Error: could not find devenv'
    746   sys.exit(1)
    747 
    748 class TestGypOnMSToolchain(TestGypBase):
    749   """
    750   Common subclass for testing generators that target the Microsoft Visual
    751   Studio toolchain (cl, link, dumpbin, etc.)
    752   """
    753   @staticmethod
    754   def _ComputeVsvarsPath(devenv_path):
    755     devenv_dir = os.path.split(devenv_path)[0]
    756     vsvars_path = os.path.join(devenv_path, '../../Tools/vsvars32.bat')
    757     return vsvars_path
    758 
    759   def initialize_build_tool(self):
    760     super(TestGypOnMSToolchain, self).initialize_build_tool()
    761     if sys.platform in ('win32', 'cygwin'):
    762       build_tools = FindVisualStudioInstallation()
    763       self.devenv_path, self.uses_msbuild, self.msbuild_path = build_tools
    764       self.vsvars_path = TestGypOnMSToolchain._ComputeVsvarsPath(
    765           self.devenv_path)
    766 
    767   def run_dumpbin(self, *dumpbin_args):
    768     """Run the dumpbin tool with the specified arguments, and capturing and
    769     returning stdout."""
    770     assert sys.platform in ('win32', 'cygwin')
    771     cmd = os.environ.get('COMSPEC', 'cmd.exe')
    772     arguments = [cmd, '/c', self.vsvars_path, '&&', 'dumpbin']
    773     arguments.extend(dumpbin_args)
    774     proc = subprocess.Popen(arguments, stdout=subprocess.PIPE)
    775     output = proc.communicate()[0]
    776     assert not proc.returncode
    777     return output
    778 
    779 class TestGypNinja(TestGypOnMSToolchain):
    780   """
    781   Subclass for testing the GYP Ninja generator.
    782   """
    783   format = 'ninja'
    784   build_tool_list = ['ninja']
    785   ALL = 'all'
    786   DEFAULT = 'all'
    787 
    788   def run_gyp(self, gyp_file, *args, **kw):
    789     TestGypBase.run_gyp(self, gyp_file, *args, **kw)
    790 
    791   def build(self, gyp_file, target=None, **kw):
    792     arguments = kw.get('arguments', [])[:]
    793 
    794     # Add a -C output/path to the command line.
    795     arguments.append('-C')
    796     arguments.append(os.path.join('out', self.configuration_dirname()))
    797 
    798     if target is None:
    799       target = 'all'
    800     arguments.append(target)
    801 
    802     kw['arguments'] = arguments
    803     return self.run(program=self.build_tool, **kw)
    804 
    805   def run_built_executable(self, name, *args, **kw):
    806     # Enclosing the name in a list avoids prepending the original dir.
    807     program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
    808     if sys.platform == 'darwin':
    809       configuration = self.configuration_dirname()
    810       os.environ['DYLD_LIBRARY_PATH'] = os.path.join('out', configuration)
    811     return self.run(program=program, *args, **kw)
    812 
    813   def built_file_path(self, name, type=None, **kw):
    814     result = []
    815     chdir = kw.get('chdir')
    816     if chdir:
    817       result.append(chdir)
    818     result.append('out')
    819     result.append(self.configuration_dirname())
    820     if type == self.STATIC_LIB:
    821       if sys.platform != 'darwin':
    822         result.append('obj')
    823     elif type == self.SHARED_LIB:
    824       if sys.platform != 'darwin' and sys.platform != 'win32':
    825         result.append('lib')
    826     subdir = kw.get('subdir')
    827     if subdir and type != self.SHARED_LIB:
    828       result.append(subdir)
    829     result.append(self.built_file_basename(name, type, **kw))
    830     return self.workpath(*result)
    831 
    832   def up_to_date(self, gyp_file, target=None, **kw):
    833     result = self.build(gyp_file, target, **kw)
    834     if not result:
    835       stdout = self.stdout()
    836       if 'ninja: no work to do' not in stdout:
    837         self.report_not_up_to_date()
    838         self.fail_test()
    839     return result
    840 
    841 
    842 class TestGypMSVS(TestGypOnMSToolchain):
    843   """
    844   Subclass for testing the GYP Visual Studio generator.
    845   """
    846   format = 'msvs'
    847 
    848   u = r'=== Build: 0 succeeded, 0 failed, (\d+) up-to-date, 0 skipped ==='
    849   up_to_date_re = re.compile(u, re.M)
    850 
    851   # Initial None element will indicate to our .initialize_build_tool()
    852   # method below that 'devenv' was not found on %PATH%.
    853   #
    854   # Note:  we must use devenv.com to be able to capture build output.
    855   # Directly executing devenv.exe only sends output to BuildLog.htm.
    856   build_tool_list = [None, 'devenv.com']
    857 
    858   def initialize_build_tool(self):
    859     super(TestGypMSVS, self).initialize_build_tool()
    860     self.build_tool = self.devenv_path
    861 
    862   def build(self, gyp_file, target=None, rebuild=False, clean=False, **kw):
    863     """
    864     Runs a Visual Studio build using the configuration generated
    865     from the specified gyp_file.
    866     """
    867     configuration = self.configuration_buildname()
    868     if clean:
    869       build = '/Clean'
    870     elif rebuild:
    871       build = '/Rebuild'
    872     else:
    873       build = '/Build'
    874     arguments = kw.get('arguments', [])[:]
    875     arguments.extend([gyp_file.replace('.gyp', '.sln'),
    876                       build, configuration])
    877     # Note:  the Visual Studio generator doesn't add an explicit 'all'
    878     # target, so we just treat it the same as the default.
    879     if target not in (None, self.ALL, self.DEFAULT):
    880       arguments.extend(['/Project', target])
    881     if self.configuration:
    882       arguments.extend(['/ProjectConfig', self.configuration])
    883     kw['arguments'] = arguments
    884     return self.run(program=self.build_tool, **kw)
    885   def up_to_date(self, gyp_file, target=None, **kw):
    886     """
    887     Verifies that a build of the specified Visual Studio target is up to date.
    888 
    889     Beware that VS2010 will behave strangely if you build under
    890     C:\USERS\yourname\AppData\Local. It will cause needless work.  The ouptut
    891     will be "1 succeeded and 0 up to date".  MSBuild tracing reveals that:
    892     "Project 'C:\Users\...\AppData\Local\...vcxproj' not up to date because
    893     'C:\PROGRAM FILES (X86)\MICROSOFT VISUAL STUDIO 10.0\VC\BIN\1033\CLUI.DLL'
    894     was modified at 02/21/2011 17:03:30, which is newer than '' which was
    895     modified at 01/01/0001 00:00:00.
    896 
    897     The workaround is to specify a workdir when instantiating the test, e.g.
    898     test = TestGyp.TestGyp(workdir='workarea')
    899     """
    900     result = self.build(gyp_file, target, **kw)
    901     if not result:
    902       stdout = self.stdout()
    903 
    904       m = self.up_to_date_re.search(stdout)
    905       up_to_date = m and int(m.group(1)) > 0
    906       if not up_to_date:
    907         self.report_not_up_to_date()
    908         self.fail_test()
    909     return result
    910   def run_built_executable(self, name, *args, **kw):
    911     """
    912     Runs an executable built by Visual Studio.
    913     """
    914     configuration = self.configuration_dirname()
    915     # Enclosing the name in a list avoids prepending the original dir.
    916     program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
    917     return self.run(program=program, *args, **kw)
    918   def built_file_path(self, name, type=None, **kw):
    919     """
    920     Returns a path to the specified file name, of the specified type,
    921     as built by Visual Studio.
    922 
    923     Built files are in a subdirectory that matches the configuration
    924     name.  The default is 'Default'.
    925 
    926     A chdir= keyword argument specifies the source directory
    927     relative to which  the output subdirectory can be found.
    928 
    929     "type" values of STATIC_LIB or SHARED_LIB append the necessary
    930     prefixes and suffixes to a platform-independent library base name.
    931     """
    932     result = []
    933     chdir = kw.get('chdir')
    934     if chdir:
    935       result.append(chdir)
    936     result.append(self.configuration_dirname())
    937     if type == self.STATIC_LIB:
    938       result.append('lib')
    939     result.append(self.built_file_basename(name, type, **kw))
    940     return self.workpath(*result)
    941 
    942 
    943 class TestGypMSVSNinja(TestGypNinja):
    944   """
    945   Subclass for testing the GYP Visual Studio Ninja generator.
    946   """
    947   format = 'msvs-ninja'
    948 
    949   def initialize_build_tool(self):
    950     super(TestGypMSVSNinja, self).initialize_build_tool()
    951     # When using '--build', make sure ninja is first in the format list.
    952     self.formats.insert(0, 'ninja')
    953 
    954   def build(self, gyp_file, target=None, rebuild=False, clean=False, **kw):
    955     """
    956     Runs a Visual Studio build using the configuration generated
    957     from the specified gyp_file.
    958     """
    959     arguments = kw.get('arguments', [])[:]
    960     if target in (None, self.ALL, self.DEFAULT):
    961       # Note: the Visual Studio generator doesn't add an explicit 'all' target.
    962       # This will build each project. This will work if projects are hermetic,
    963       # but may fail if they are not (a project may run more than once).
    964       # It would be nice to supply an all.metaproj for MSBuild.
    965       arguments.extend([gyp_file.replace('.gyp', '.sln')])
    966     else:
    967       # MSBuild documentation claims that one can specify a sln but then build a
    968       # project target like 'msbuild a.sln /t:proj:target' but this format only
    969       # supports 'Clean', 'Rebuild', and 'Publish' (with none meaning Default).
    970       # This limitation is due to the .sln -> .sln.metaproj conversion.
    971       # The ':' is not special, 'proj:target' is a target in the metaproj.
    972       arguments.extend([target+'.vcxproj'])
    973 
    974     if clean:
    975       build = 'Clean'
    976     elif rebuild:
    977       build = 'Rebuild'
    978     else:
    979       build = 'Build'
    980     arguments.extend(['/target:'+build])
    981     configuration = self.configuration_buildname()
    982     config = configuration.split('|')
    983     arguments.extend(['/property:Configuration='+config[0]])
    984     if len(config) > 1:
    985       arguments.extend(['/property:Platform='+config[1]])
    986     arguments.extend(['/property:BuildInParallel=false'])
    987     arguments.extend(['/verbosity:minimal'])
    988 
    989     kw['arguments'] = arguments
    990     return self.run(program=self.msbuild_path, **kw)
    991 
    992 
    993 class TestGypXcode(TestGypBase):
    994   """
    995   Subclass for testing the GYP Xcode generator.
    996   """
    997   format = 'xcode'
    998   build_tool_list = ['xcodebuild']
    999 
   1000   phase_script_execution = ("\n"
   1001                             "PhaseScriptExecution /\\S+/Script-[0-9A-F]+\\.sh\n"
   1002                             "    cd /\\S+\n"
   1003                             "    /bin/sh -c /\\S+/Script-[0-9A-F]+\\.sh\n"
   1004                             "(make: Nothing to be done for `all'\\.\n)?")
   1005 
   1006   strip_up_to_date_expressions = [
   1007     # Various actions or rules can run even when the overall build target
   1008     # is up to date.  Strip those phases' GYP-generated output.
   1009     re.compile(phase_script_execution, re.S),
   1010 
   1011     # The message from distcc_pump can trail the "BUILD SUCCEEDED"
   1012     # message, so strip that, too.
   1013     re.compile('__________Shutting down distcc-pump include server\n', re.S),
   1014   ]
   1015 
   1016   up_to_date_endings = (
   1017     'Checking Dependencies...\n** BUILD SUCCEEDED **\n', # Xcode 3.0/3.1
   1018     'Check dependencies\n** BUILD SUCCEEDED **\n\n',     # Xcode 3.2
   1019     'Check dependencies\n\n\n** BUILD SUCCEEDED **\n\n', # Xcode 4.2
   1020     'Check dependencies\n\n** BUILD SUCCEEDED **\n\n',   # Xcode 5.0
   1021   )
   1022 
   1023   def build(self, gyp_file, target=None, **kw):
   1024     """
   1025     Runs an xcodebuild using the .xcodeproj generated from the specified
   1026     gyp_file.
   1027     """
   1028     # Be sure we're working with a copy of 'arguments' since we modify it.
   1029     # The caller may not be expecting it to be modified.
   1030     arguments = kw.get('arguments', [])[:]
   1031     arguments.extend(['-project', gyp_file.replace('.gyp', '.xcodeproj')])
   1032     if target == self.ALL:
   1033       arguments.append('-alltargets',)
   1034     elif target not in (None, self.DEFAULT):
   1035       arguments.extend(['-target', target])
   1036     if self.configuration:
   1037       arguments.extend(['-configuration', self.configuration])
   1038     symroot = kw.get('SYMROOT', '$SRCROOT/build')
   1039     if symroot:
   1040       arguments.append('SYMROOT='+symroot)
   1041     kw['arguments'] = arguments
   1042 
   1043     # Work around spurious stderr output from Xcode 4, http://crbug.com/181012
   1044     match = kw.pop('match', self.match)
   1045     def match_filter_xcode(actual, expected):
   1046       if actual:
   1047         if not TestCmd.is_List(actual):
   1048           actual = actual.split('\n')
   1049         if not TestCmd.is_List(expected):
   1050           expected = expected.split('\n')
   1051         actual = [a for a in actual
   1052                     if 'No recorder, buildTask: <Xcode3BuildTask:' not in a and
   1053                        'Beginning test session' not in a]
   1054       return match(actual, expected)
   1055     kw['match'] = match_filter_xcode
   1056 
   1057     return self.run(program=self.build_tool, **kw)
   1058   def up_to_date(self, gyp_file, target=None, **kw):
   1059     """
   1060     Verifies that a build of the specified Xcode target is up to date.
   1061     """
   1062     result = self.build(gyp_file, target, **kw)
   1063     if not result:
   1064       output = self.stdout()
   1065       for expression in self.strip_up_to_date_expressions:
   1066         output = expression.sub('', output)
   1067       if not output.endswith(self.up_to_date_endings):
   1068         self.report_not_up_to_date()
   1069         self.fail_test()
   1070     return result
   1071   def run_built_executable(self, name, *args, **kw):
   1072     """
   1073     Runs an executable built by xcodebuild.
   1074     """
   1075     configuration = self.configuration_dirname()
   1076     os.environ['DYLD_LIBRARY_PATH'] = os.path.join('build', configuration)
   1077     # Enclosing the name in a list avoids prepending the original dir.
   1078     program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
   1079     return self.run(program=program, *args, **kw)
   1080   def built_file_path(self, name, type=None, **kw):
   1081     """
   1082     Returns a path to the specified file name, of the specified type,
   1083     as built by Xcode.
   1084 
   1085     Built files are in the subdirectory 'build/{configuration}'.
   1086     The default is 'build/Default'.
   1087 
   1088     A chdir= keyword argument specifies the source directory
   1089     relative to which  the output subdirectory can be found.
   1090 
   1091     "type" values of STATIC_LIB or SHARED_LIB append the necessary
   1092     prefixes and suffixes to a platform-independent library base name.
   1093     """
   1094     result = []
   1095     chdir = kw.get('chdir')
   1096     if chdir:
   1097       result.append(chdir)
   1098     configuration = self.configuration_dirname()
   1099     result.extend(['build', configuration])
   1100     result.append(self.built_file_basename(name, type, **kw))
   1101     return self.workpath(*result)
   1102 
   1103 
   1104 class TestGypXcodeNinja(TestGypXcode):
   1105   """
   1106   Subclass for testing the GYP Xcode Ninja generator.
   1107   """
   1108   format = 'xcode-ninja'
   1109 
   1110   def initialize_build_tool(self):
   1111     super(TestGypXcodeNinja, self).initialize_build_tool()
   1112     # When using '--build', make sure ninja is first in the format list.
   1113     self.formats.insert(0, 'ninja')
   1114 
   1115   def build(self, gyp_file, target=None, **kw):
   1116     """
   1117     Runs an xcodebuild using the .xcodeproj generated from the specified
   1118     gyp_file.
   1119     """
   1120     build_config = self.configuration
   1121     if build_config and build_config.endswith(('-iphoneos',
   1122                                                '-iphonesimulator')):
   1123       build_config, sdk = self.configuration.split('-')
   1124       kw['arguments'] = kw.get('arguments', []) + ['-sdk', sdk]
   1125 
   1126     with self._build_configuration(build_config):
   1127       return super(TestGypXcodeNinja, self).build(
   1128         gyp_file.replace('.gyp', '.ninja.gyp'), target, **kw)
   1129 
   1130   @contextmanager
   1131   def _build_configuration(self, build_config):
   1132     config = self.configuration
   1133     self.configuration = build_config
   1134     try:
   1135       yield
   1136     finally:
   1137       self.configuration = config
   1138 
   1139   def built_file_path(self, name, type=None, **kw):
   1140     result = []
   1141     chdir = kw.get('chdir')
   1142     if chdir:
   1143       result.append(chdir)
   1144     result.append('out')
   1145     result.append(self.configuration_dirname())
   1146     subdir = kw.get('subdir')
   1147     if subdir and type != self.SHARED_LIB:
   1148       result.append(subdir)
   1149     result.append(self.built_file_basename(name, type, **kw))
   1150     return self.workpath(*result)
   1151 
   1152   def up_to_date(self, gyp_file, target=None, **kw):
   1153     result = self.build(gyp_file, target, **kw)
   1154     if not result:
   1155       stdout = self.stdout()
   1156       if 'ninja: no work to do' not in stdout:
   1157         self.report_not_up_to_date()
   1158         self.fail_test()
   1159     return result
   1160 
   1161   def run_built_executable(self, name, *args, **kw):
   1162     """
   1163     Runs an executable built by xcodebuild + ninja.
   1164     """
   1165     configuration = self.configuration_dirname()
   1166     os.environ['DYLD_LIBRARY_PATH'] = os.path.join('out', configuration)
   1167     # Enclosing the name in a list avoids prepending the original dir.
   1168     program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
   1169     return self.run(program=program, *args, **kw)
   1170 
   1171 
   1172 format_class_list = [
   1173   TestGypGypd,
   1174   TestGypCMake,
   1175   TestGypMake,
   1176   TestGypMSVS,
   1177   TestGypMSVSNinja,
   1178   TestGypNinja,
   1179   TestGypXcode,
   1180   TestGypXcodeNinja,
   1181 ]
   1182 
   1183 def TestGyp(*args, **kw):
   1184   """
   1185   Returns an appropriate TestGyp* instance for a specified GYP format.
   1186   """
   1187   format = kw.pop('format', os.environ.get('TESTGYP_FORMAT'))
   1188   for format_class in format_class_list:
   1189     if format == format_class.format:
   1190       return format_class(*args, **kw)
   1191   raise Exception, "unknown format %r" % format
   1192