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 os
     10 import re
     11 import shutil
     12 import stat
     13 import subprocess
     14 import sys
     15 import tempfile
     16 
     17 import TestCmd
     18 import TestCommon
     19 from TestCommon import __all__
     20 
     21 __all__.extend([
     22   'TestGyp',
     23 ])
     24 
     25 def remove_debug_line_numbers(contents):
     26   """Function to remove the line numbers from the debug output
     27   of gyp and thus remove the exremem fragility of the stdout
     28   comparison tests.
     29   """
     30   lines = contents.splitlines()
     31   # split each line on ":"
     32   lines = [l.split(":", 3) for l in lines]
     33   # join each line back together while ignoring the
     34   # 3rd column which is the line number
     35   lines = [len(l) > 3 and ":".join(l[3:]) or l for l in lines]
     36   return "\n".join(lines)
     37 
     38 def match_modulo_line_numbers(contents_a, contents_b):
     39   """File contents matcher that ignores line numbers."""
     40   contents_a = remove_debug_line_numbers(contents_a)
     41   contents_b = remove_debug_line_numbers(contents_b)
     42   return TestCommon.match_exact(contents_a, contents_b)
     43 
     44 class TestGypBase(TestCommon.TestCommon):
     45   """
     46   Class for controlling end-to-end tests of gyp generators.
     47 
     48   Instantiating this class will create a temporary directory and
     49   arrange for its destruction (via the TestCmd superclass) and
     50   copy all of the non-gyptest files in the directory hierarchy of the
     51   executing script.
     52 
     53   The default behavior is to test the 'gyp' or 'gyp.bat' file in the
     54   current directory.  An alternative may be specified explicitly on
     55   instantiation, or by setting the TESTGYP_GYP environment variable.
     56 
     57   This class should be subclassed for each supported gyp generator
     58   (format).  Various abstract methods below define calling signatures
     59   used by the test scripts to invoke builds on the generated build
     60   configuration and to run executables generated by those builds.
     61   """
     62 
     63   build_tool = None
     64   build_tool_list = []
     65 
     66   _exe = TestCommon.exe_suffix
     67   _obj = TestCommon.obj_suffix
     68   shobj_ = TestCommon.shobj_prefix
     69   _shobj = TestCommon.shobj_suffix
     70   lib_ = TestCommon.lib_prefix
     71   _lib = TestCommon.lib_suffix
     72   dll_ = TestCommon.dll_prefix
     73   _dll = TestCommon.dll_suffix
     74 
     75   # Constants to represent different targets.
     76   ALL = '__all__'
     77   DEFAULT = '__default__'
     78 
     79   # Constants for different target types.
     80   EXECUTABLE = '__executable__'
     81   STATIC_LIB = '__static_lib__'
     82   SHARED_LIB = '__shared_lib__'
     83 
     84   def __init__(self, gyp=None, *args, **kw):
     85     self.origin_cwd = os.path.abspath(os.path.dirname(sys.argv[0]))
     86     self.extra_args = sys.argv[1:]
     87 
     88     if not gyp:
     89       gyp = os.environ.get('TESTGYP_GYP')
     90       if not gyp:
     91         if sys.platform == 'win32':
     92           gyp = 'gyp.bat'
     93         else:
     94           gyp = 'gyp'
     95     self.gyp = os.path.abspath(gyp)
     96 
     97     self.initialize_build_tool()
     98 
     99     kw.setdefault('match', TestCommon.match_exact)
    100 
    101     # Put test output in out/testworkarea by default.
    102     # Use temporary names so there are no collisions.
    103     workdir = os.path.join('out', kw.get('workdir', 'testworkarea'))
    104     # Create work area if it doesn't already exist.
    105     if not os.path.isdir(workdir):
    106       os.makedirs(workdir)
    107 
    108     kw['workdir'] = tempfile.mktemp(prefix='testgyp.', dir=workdir)
    109 
    110     formats = kw.pop('formats', [])
    111 
    112     super(TestGypBase, self).__init__(*args, **kw)
    113 
    114     excluded_formats = set([f for f in formats if f[0] == '!'])
    115     included_formats = set(formats) - excluded_formats
    116     if ('!'+self.format in excluded_formats or
    117         included_formats and self.format not in included_formats):
    118       msg = 'Invalid test for %r format; skipping test.\n'
    119       self.skip_test(msg % self.format)
    120 
    121     self.copy_test_configuration(self.origin_cwd, self.workdir)
    122     self.set_configuration(None)
    123 
    124     # Set $HOME so that gyp doesn't read the user's actual
    125     # ~/.gyp/include.gypi file, which may contain variables
    126     # and other settings that would change the output.
    127     os.environ['HOME'] = self.workpath()
    128     # Clear $GYP_DEFINES for the same reason.
    129     if 'GYP_DEFINES' in os.environ:
    130       del os.environ['GYP_DEFINES']
    131 
    132   def built_file_must_exist(self, name, type=None, **kw):
    133     """
    134     Fails the test if the specified built file name does not exist.
    135     """
    136     return self.must_exist(self.built_file_path(name, type, **kw))
    137 
    138   def built_file_must_not_exist(self, name, type=None, **kw):
    139     """
    140     Fails the test if the specified built file name exists.
    141     """
    142     return self.must_not_exist(self.built_file_path(name, type, **kw))
    143 
    144   def built_file_must_match(self, name, contents, **kw):
    145     """
    146     Fails the test if the contents of the specified built file name
    147     do not match the specified contents.
    148     """
    149     return self.must_match(self.built_file_path(name, **kw), contents)
    150 
    151   def built_file_must_not_match(self, name, contents, **kw):
    152     """
    153     Fails the test if the contents of the specified built file name
    154     match the specified contents.
    155     """
    156     return self.must_not_match(self.built_file_path(name, **kw), contents)
    157 
    158   def copy_test_configuration(self, source_dir, dest_dir):
    159     """
    160     Copies the test configuration from the specified source_dir
    161     (the directory in which the test script lives) to the
    162     specified dest_dir (a temporary working directory).
    163 
    164     This ignores all files and directories that begin with
    165     the string 'gyptest', and all '.svn' subdirectories.
    166     """
    167     for root, dirs, files in os.walk(source_dir):
    168       if '.svn' in dirs:
    169         dirs.remove('.svn')
    170       dirs = [ d for d in dirs if not d.startswith('gyptest') ]
    171       files = [ f for f in files if not f.startswith('gyptest') ]
    172       for dirname in dirs:
    173         source = os.path.join(root, dirname)
    174         destination = source.replace(source_dir, dest_dir)
    175         os.mkdir(destination)
    176         if sys.platform != 'win32':
    177           shutil.copystat(source, destination)
    178       for filename in files:
    179         source = os.path.join(root, filename)
    180         destination = source.replace(source_dir, dest_dir)
    181         shutil.copy2(source, destination)
    182 
    183   def initialize_build_tool(self):
    184     """
    185     Initializes the .build_tool attribute.
    186 
    187     Searches the .build_tool_list for an executable name on the user's
    188     $PATH.  The first tool on the list is used as-is if nothing is found
    189     on the current $PATH.
    190     """
    191     for build_tool in self.build_tool_list:
    192       if not build_tool:
    193         continue
    194       if os.path.isabs(build_tool):
    195         self.build_tool = build_tool
    196         return
    197       build_tool = self.where_is(build_tool)
    198       if build_tool:
    199         self.build_tool = build_tool
    200         return
    201 
    202     if self.build_tool_list:
    203       self.build_tool = self.build_tool_list[0]
    204 
    205   def relocate(self, source, destination):
    206     """
    207     Renames (relocates) the specified source (usually a directory)
    208     to the specified destination, creating the destination directory
    209     first if necessary.
    210 
    211     Note:  Don't use this as a generic "rename" operation.  In the
    212     future, "relocating" parts of a GYP tree may affect the state of
    213     the test to modify the behavior of later method calls.
    214     """
    215     destination_dir = os.path.dirname(destination)
    216     if not os.path.exists(destination_dir):
    217       self.subdir(destination_dir)
    218     os.rename(source, destination)
    219 
    220   def report_not_up_to_date(self):
    221     """
    222     Reports that a build is not up-to-date.
    223 
    224     This provides common reporting for formats that have complicated
    225     conditions for checking whether a build is up-to-date.  Formats
    226     that expect exact output from the command (make) can
    227     just set stdout= when they call the run_build() method.
    228     """
    229     print "Build is not up-to-date:"
    230     print self.banner('STDOUT ')
    231     print self.stdout()
    232     stderr = self.stderr()
    233     if stderr:
    234       print self.banner('STDERR ')
    235       print stderr
    236 
    237   def run_gyp(self, gyp_file, *args, **kw):
    238     """
    239     Runs gyp against the specified gyp_file with the specified args.
    240     """
    241 
    242     # When running gyp, and comparing its output we use a comparitor
    243     # that ignores the line numbers that gyp logs in its debug output.
    244     if kw.pop('ignore_line_numbers', False):
    245       kw.setdefault('match', match_modulo_line_numbers)
    246 
    247     # TODO:  --depth=. works around Chromium-specific tree climbing.
    248     depth = kw.pop('depth', '.')
    249     run_args = ['--depth='+depth, '--format='+self.format, gyp_file]
    250     run_args.extend(self.extra_args)
    251     run_args.extend(args)
    252     return self.run(program=self.gyp, arguments=run_args, **kw)
    253 
    254   def run(self, *args, **kw):
    255     """
    256     Executes a program by calling the superclass .run() method.
    257 
    258     This exists to provide a common place to filter out keyword
    259     arguments implemented in this layer, without having to update
    260     the tool-specific subclasses or clutter the tests themselves
    261     with platform-specific code.
    262     """
    263     if kw.has_key('SYMROOT'):
    264       del kw['SYMROOT']
    265     super(TestGypBase, self).run(*args, **kw)
    266 
    267   def set_configuration(self, configuration):
    268     """
    269     Sets the configuration, to be used for invoking the build
    270     tool and testing potential built output.
    271     """
    272     self.configuration = configuration
    273 
    274   def configuration_dirname(self):
    275     if self.configuration:
    276       return self.configuration.split('|')[0]
    277     else:
    278       return 'Default'
    279 
    280   def configuration_buildname(self):
    281     if self.configuration:
    282       return self.configuration
    283     else:
    284       return 'Default'
    285 
    286   #
    287   # Abstract methods to be defined by format-specific subclasses.
    288   #
    289 
    290   def build(self, gyp_file, target=None, **kw):
    291     """
    292     Runs a build of the specified target against the configuration
    293     generated from the specified gyp_file.
    294 
    295     A 'target' argument of None or the special value TestGyp.DEFAULT
    296     specifies the default argument for the underlying build tool.
    297     A 'target' argument of TestGyp.ALL specifies the 'all' target
    298     (if any) of the underlying build tool.
    299     """
    300     raise NotImplementedError
    301 
    302   def built_file_path(self, name, type=None, **kw):
    303     """
    304     Returns a path to the specified file name, of the specified type.
    305     """
    306     raise NotImplementedError
    307 
    308   def built_file_basename(self, name, type=None, **kw):
    309     """
    310     Returns the base name of the specified file name, of the specified type.
    311 
    312     A bare=True keyword argument specifies that prefixes and suffixes shouldn't
    313     be applied.
    314     """
    315     if not kw.get('bare'):
    316       if type == self.EXECUTABLE:
    317         name = name + self._exe
    318       elif type == self.STATIC_LIB:
    319         name = self.lib_ + name + self._lib
    320       elif type == self.SHARED_LIB:
    321         name = self.dll_ + name + self._dll
    322     return name
    323 
    324   def run_built_executable(self, name, *args, **kw):
    325     """
    326     Runs an executable program built from a gyp-generated configuration.
    327 
    328     The specified name should be independent of any particular generator.
    329     Subclasses should find the output executable in the appropriate
    330     output build directory, tack on any necessary executable suffix, etc.
    331     """
    332     raise NotImplementedError
    333 
    334   def up_to_date(self, gyp_file, target=None, **kw):
    335     """
    336     Verifies that a build of the specified target is up to date.
    337 
    338     The subclass should implement this by calling build()
    339     (or a reasonable equivalent), checking whatever conditions
    340     will tell it the build was an "up to date" null build, and
    341     failing if it isn't.
    342     """
    343     raise NotImplementedError
    344 
    345 
    346 class TestGypGypd(TestGypBase):
    347   """
    348   Subclass for testing the GYP 'gypd' generator (spit out the
    349   internal data structure as pretty-printed Python).
    350   """
    351   format = 'gypd'
    352 
    353 
    354 class TestGypCustom(TestGypBase):
    355   """
    356   Subclass for testing the GYP with custom generator
    357   """
    358 
    359   def __init__(self, gyp=None, *args, **kw):
    360     self.format = kw.pop("format")
    361     super(TestGypCustom, self).__init__(*args, **kw)
    362 
    363 
    364 class TestGypAndroid(TestGypBase):
    365   """
    366   Subclass for testing the GYP Android makefile generator. Note that
    367   build/envsetup.sh and lunch must have been run before running tests.
    368 
    369   TODO: This is currently an incomplete implementation. We do not support
    370   run_built_executable(), so we pass only tests which do not use this. As a
    371   result, support for host targets is not properly tested.
    372   """
    373   format = 'android'
    374 
    375   # Note that we can't use mmm as the build tool because ...
    376   # - it builds all targets, whereas we need to pass a target
    377   # - it is a function, whereas the test runner assumes the build tool is a file
    378   # Instead we use make and duplicate the logic from mmm.
    379   build_tool_list = ['make']
    380 
    381   # We use our custom target 'gyp_all_modules', as opposed to the 'all_modules'
    382   # target used by mmm, to build only those targets which are part of the gyp
    383   # target 'all'.
    384   ALL = 'gyp_all_modules'
    385 
    386   def __init__(self, gyp=None, *args, **kw):
    387     # Android requires build and test output to be inside its source tree.
    388     # We use the following working directory for the test's source, but the
    389     # test's build output still goes to $ANDROID_PRODUCT_OUT.
    390     # Note that some tests explicitly set format='gypd' to invoke the gypd
    391     # backend. This writes to the source tree, but there's no way around this.
    392     kw['workdir'] = os.path.join('/tmp', 'gyptest',
    393                                  kw.get('workdir', 'testworkarea'))
    394     # We need to remove all gyp outputs from out/. Ths is because some tests
    395     # don't have rules to regenerate output, so they will simply re-use stale
    396     # output if present. Since the test working directory gets regenerated for
    397     # each test run, this can confuse things.
    398     # We don't have a list of build outputs because we don't know which
    399     # dependent targets were built. Instead we delete all gyp-generated output.
    400     # This may be excessive, but should be safe.
    401     out_dir = os.environ['ANDROID_PRODUCT_OUT']
    402     obj_dir = os.path.join(out_dir, 'obj')
    403     shutil.rmtree(os.path.join(obj_dir, 'GYP'), ignore_errors = True)
    404     for x in ['EXECUTABLES', 'STATIC_LIBRARIES', 'SHARED_LIBRARIES']:
    405       for d in os.listdir(os.path.join(obj_dir, x)):
    406         if d.endswith('_gyp_intermediates'):
    407           shutil.rmtree(os.path.join(obj_dir, x, d), ignore_errors = True)
    408     for x in [os.path.join('obj', 'lib'), os.path.join('system', 'lib')]:
    409       for d in os.listdir(os.path.join(out_dir, x)):
    410         if d.endswith('_gyp.so'):
    411           os.remove(os.path.join(out_dir, x, d))
    412 
    413     super(TestGypAndroid, self).__init__(*args, **kw)
    414 
    415   def target_name(self, target):
    416     if target == self.ALL:
    417       return self.ALL
    418     # The default target is 'droid'. However, we want to use our special target
    419     # to build only the gyp target 'all'.
    420     if target in (None, self.DEFAULT):
    421       return self.ALL
    422     return target
    423 
    424   def build(self, gyp_file, target=None, **kw):
    425     """
    426     Runs a build using the Android makefiles generated from the specified
    427     gyp_file. This logic is taken from Android's mmm.
    428     """
    429     arguments = kw.get('arguments', [])[:]
    430     arguments.append(self.target_name(target))
    431     arguments.append('-C')
    432     arguments.append(os.environ['ANDROID_BUILD_TOP'])
    433     kw['arguments'] = arguments
    434     chdir = kw.get('chdir', '')
    435     makefile = os.path.join(self.workdir, chdir, 'GypAndroid.mk')
    436     os.environ['ONE_SHOT_MAKEFILE'] = makefile
    437     result = self.run(program=self.build_tool, **kw)
    438     del os.environ['ONE_SHOT_MAKEFILE']
    439     return result
    440 
    441   def android_module(self, group, name, subdir):
    442     if subdir:
    443       name = '%s_%s' % (subdir, name)
    444     if group == 'SHARED_LIBRARIES':
    445       name = 'lib_%s' % name
    446     return '%s_gyp' % name
    447 
    448   def intermediates_dir(self, group, module_name):
    449     return os.path.join(os.environ['ANDROID_PRODUCT_OUT'], 'obj', group,
    450                         '%s_intermediates' % module_name)
    451 
    452   def built_file_path(self, name, type=None, **kw):
    453     """
    454     Returns a path to the specified file name, of the specified type,
    455     as built by Android. Note that we don't support the configuration
    456     parameter.
    457     """
    458     # Built files are in $ANDROID_PRODUCT_OUT. This requires copying logic from
    459     # the Android build system.
    460     if type == None:
    461       return os.path.join(os.environ['ANDROID_PRODUCT_OUT'], 'obj', 'GYP',
    462                           'shared_intermediates', name)
    463     subdir = kw.get('subdir')
    464     if type == self.EXECUTABLE:
    465       # We don't install executables
    466       group = 'EXECUTABLES'
    467       module_name = self.android_module(group, name, subdir)
    468       return os.path.join(self.intermediates_dir(group, module_name), name)
    469     if type == self.STATIC_LIB:
    470       group = 'STATIC_LIBRARIES'
    471       module_name = self.android_module(group, name, subdir)
    472       return os.path.join(self.intermediates_dir(group, module_name),
    473                           '%s.a' % module_name)
    474     if type == self.SHARED_LIB:
    475       group = 'SHARED_LIBRARIES'
    476       module_name = self.android_module(group, name, subdir)
    477       return os.path.join(self.intermediates_dir(group, module_name), 'LINKED',
    478                           '%s.so' % module_name)
    479     assert False, 'Unhandled type'
    480 
    481   def run_built_executable(self, name, *args, **kw):
    482     """
    483     Runs an executable program built from a gyp-generated configuration.
    484 
    485     This is not correctly implemented for Android. For now, we simply check
    486     that the executable file exists.
    487     """
    488     # Running executables requires a device. Even if we build for target x86,
    489     # the binary is not built with the correct toolchain options to actually
    490     # run on the host.
    491 
    492     # Copied from TestCommon.run()
    493     match = kw.pop('match', self.match)
    494     status = None
    495     if os.path.exists(self.built_file_path(name)):
    496       status = 1
    497     self._complete(None, None, None, None, status, match)
    498 
    499   def match_single_line(self, lines = None, expected_line = None):
    500     """
    501     Checks that specified line appears in the text.
    502     """
    503     for line in lines.split('\n'):
    504         if line == expected_line:
    505             return 1
    506     return
    507 
    508   def up_to_date(self, gyp_file, target=None, **kw):
    509     """
    510     Verifies that a build of the specified target is up to date.
    511     """
    512     kw['stdout'] = ("make: Nothing to be done for `%s'." %
    513                     self.target_name(target))
    514 
    515     # We need to supply a custom matcher, since we don't want to depend on the
    516     # exact stdout string.
    517     kw['match'] = self.match_single_line
    518     return self.build(gyp_file, target, **kw)
    519 
    520 class TestGypMake(TestGypBase):
    521   """
    522   Subclass for testing the GYP Make generator.
    523   """
    524   format = 'make'
    525   build_tool_list = ['make']
    526   ALL = 'all'
    527   def build(self, gyp_file, target=None, **kw):
    528     """
    529     Runs a Make build using the Makefiles generated from the specified
    530     gyp_file.
    531     """
    532     arguments = kw.get('arguments', [])[:]
    533     if self.configuration:
    534       arguments.append('BUILDTYPE=' + self.configuration)
    535     if target not in (None, self.DEFAULT):
    536       arguments.append(target)
    537     # Sub-directory builds provide per-gyp Makefiles (i.e.
    538     # Makefile.gyp_filename), so use that if there is no Makefile.
    539     chdir = kw.get('chdir', '')
    540     if not os.path.exists(os.path.join(chdir, 'Makefile')):
    541       print "NO Makefile in " + os.path.join(chdir, 'Makefile')
    542       arguments.insert(0, '-f')
    543       arguments.insert(1, os.path.splitext(gyp_file)[0] + '.Makefile')
    544     kw['arguments'] = arguments
    545     return self.run(program=self.build_tool, **kw)
    546   def up_to_date(self, gyp_file, target=None, **kw):
    547     """
    548     Verifies that a build of the specified Make target is up to date.
    549     """
    550     if target in (None, self.DEFAULT):
    551       message_target = 'all'
    552     else:
    553       message_target = target
    554     kw['stdout'] = "make: Nothing to be done for `%s'.\n" % message_target
    555     return self.build(gyp_file, target, **kw)
    556   def run_built_executable(self, name, *args, **kw):
    557     """
    558     Runs an executable built by Make.
    559     """
    560     configuration = self.configuration_dirname()
    561     libdir = os.path.join('out', configuration, 'lib')
    562     # TODO(piman): when everything is cross-compile safe, remove lib.target
    563     if sys.platform == 'darwin':
    564       # Mac puts target shared libraries right in the product directory.
    565       configuration = self.configuration_dirname()
    566       os.environ['DYLD_LIBRARY_PATH'] = (
    567           libdir + '.host:' + os.path.join('out', configuration))
    568     else:
    569       os.environ['LD_LIBRARY_PATH'] = libdir + '.host:' + libdir + '.target'
    570     # Enclosing the name in a list avoids prepending the original dir.
    571     program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
    572     return self.run(program=program, *args, **kw)
    573   def built_file_path(self, name, type=None, **kw):
    574     """
    575     Returns a path to the specified file name, of the specified type,
    576     as built by Make.
    577 
    578     Built files are in the subdirectory 'out/{configuration}'.
    579     The default is 'out/Default'.
    580 
    581     A chdir= keyword argument specifies the source directory
    582     relative to which  the output subdirectory can be found.
    583 
    584     "type" values of STATIC_LIB or SHARED_LIB append the necessary
    585     prefixes and suffixes to a platform-independent library base name.
    586 
    587     A subdir= keyword argument specifies a library subdirectory within
    588     the default 'obj.target'.
    589     """
    590     result = []
    591     chdir = kw.get('chdir')
    592     if chdir:
    593       result.append(chdir)
    594     configuration = self.configuration_dirname()
    595     result.extend(['out', configuration])
    596     if type == self.STATIC_LIB and sys.platform != 'darwin':
    597       result.append('obj.target')
    598     elif type == self.SHARED_LIB and sys.platform != 'darwin':
    599       result.append('lib.target')
    600     subdir = kw.get('subdir')
    601     if subdir and type != self.SHARED_LIB:
    602       result.append(subdir)
    603     result.append(self.built_file_basename(name, type, **kw))
    604     return self.workpath(*result)
    605 
    606 
    607 def ConvertToCygpath(path):
    608   """Convert to cygwin path if we are using cygwin."""
    609   if sys.platform == 'cygwin':
    610     p = subprocess.Popen(['cygpath', path], stdout=subprocess.PIPE)
    611     path = p.communicate()[0].strip()
    612   return path
    613 
    614 
    615 def FindVisualStudioInstallation():
    616   """Returns appropriate values for .build_tool and .uses_msbuild fields
    617   of TestGypBase for Visual Studio.
    618 
    619   We use the value specified by GYP_MSVS_VERSION.  If not specified, we
    620   search %PATH% and %PATHEXT% for a devenv.{exe,bat,...} executable.
    621   Failing that, we search for likely deployment paths.
    622   """
    623   possible_roots = ['%s:\\Program Files%s' % (chr(drive), suffix)
    624                     for drive in range(ord('C'), ord('Z') + 1)
    625                     for suffix in ['', ' (x86)']]
    626   possible_paths = {
    627       '2012': r'Microsoft Visual Studio 11.0\Common7\IDE\devenv.com',
    628       '2010': r'Microsoft Visual Studio 10.0\Common7\IDE\devenv.com',
    629       '2008': r'Microsoft Visual Studio 9.0\Common7\IDE\devenv.com',
    630       '2005': r'Microsoft Visual Studio 8\Common7\IDE\devenv.com'}
    631 
    632   possible_roots = [ConvertToCygpath(r) for r in possible_roots]
    633 
    634   msvs_version = 'auto'
    635   for flag in (f for f in sys.argv if f.startswith('msvs_version=')):
    636     msvs_version = flag.split('=')[-1]
    637   msvs_version = os.environ.get('GYP_MSVS_VERSION', msvs_version)
    638 
    639   build_tool = None
    640   if msvs_version in possible_paths:
    641     # Check that the path to the specified GYP_MSVS_VERSION exists.
    642     path = possible_paths[msvs_version]
    643     for r in possible_roots:
    644       bt = os.path.join(r, path)
    645       if os.path.exists(bt):
    646         build_tool = bt
    647         uses_msbuild = msvs_version >= '2010'
    648         return build_tool, uses_msbuild
    649     else:
    650       print ('Warning: Environment variable GYP_MSVS_VERSION specifies "%s" '
    651               'but corresponding "%s" was not found.' % (msvs_version, path))
    652   if build_tool:
    653     # We found 'devenv' on the path, use that and try to guess the version.
    654     for version, path in possible_paths.iteritems():
    655       if build_tool.find(path) >= 0:
    656         uses_msbuild = version >= '2010'
    657         return build_tool, uses_msbuild
    658     else:
    659       # If not, assume not MSBuild.
    660       uses_msbuild = False
    661     return build_tool, uses_msbuild
    662   # Neither GYP_MSVS_VERSION nor the path help us out.  Iterate through
    663   # the choices looking for a match.
    664   for version in sorted(possible_paths, reverse=True):
    665     path = possible_paths[version]
    666     for r in possible_roots:
    667       bt = os.path.join(r, path)
    668       if os.path.exists(bt):
    669         build_tool = bt
    670         uses_msbuild = msvs_version >= '2010'
    671         return build_tool, uses_msbuild
    672   print 'Error: could not find devenv'
    673   sys.exit(1)
    674 
    675 class TestGypOnMSToolchain(TestGypBase):
    676   """
    677   Common subclass for testing generators that target the Microsoft Visual
    678   Studio toolchain (cl, link, dumpbin, etc.)
    679   """
    680   @staticmethod
    681   def _ComputeVsvarsPath(devenv_path):
    682     devenv_dir = os.path.split(devenv_path)[0]
    683     vsvars_path = os.path.join(devenv_path, '../../Tools/vsvars32.bat')
    684     return vsvars_path
    685 
    686   def initialize_build_tool(self):
    687     super(TestGypOnMSToolchain, self).initialize_build_tool()
    688     if sys.platform in ('win32', 'cygwin'):
    689       self.devenv_path, self.uses_msbuild = FindVisualStudioInstallation()
    690       self.vsvars_path = TestGypOnMSToolchain._ComputeVsvarsPath(
    691           self.devenv_path)
    692 
    693   def run_dumpbin(self, *dumpbin_args):
    694     """Run the dumpbin tool with the specified arguments, and capturing and
    695     returning stdout."""
    696     assert sys.platform in ('win32', 'cygwin')
    697     cmd = os.environ.get('COMSPEC', 'cmd.exe')
    698     arguments = [cmd, '/c', self.vsvars_path, '&&', 'dumpbin']
    699     arguments.extend(dumpbin_args)
    700     proc = subprocess.Popen(arguments, stdout=subprocess.PIPE)
    701     output = proc.communicate()[0]
    702     assert not proc.returncode
    703     return output
    704 
    705 class TestGypNinja(TestGypOnMSToolchain):
    706   """
    707   Subclass for testing the GYP Ninja generator.
    708   """
    709   format = 'ninja'
    710   build_tool_list = ['ninja']
    711   ALL = 'all'
    712   DEFAULT = 'all'
    713 
    714   def run_gyp(self, gyp_file, *args, **kw):
    715     TestGypBase.run_gyp(self, gyp_file, *args, **kw)
    716 
    717   def build(self, gyp_file, target=None, **kw):
    718     arguments = kw.get('arguments', [])[:]
    719 
    720     # Add a -C output/path to the command line.
    721     arguments.append('-C')
    722     arguments.append(os.path.join('out', self.configuration_dirname()))
    723 
    724     if target is None:
    725       target = 'all'
    726     arguments.append(target)
    727 
    728     kw['arguments'] = arguments
    729     return self.run(program=self.build_tool, **kw)
    730 
    731   def run_built_executable(self, name, *args, **kw):
    732     # Enclosing the name in a list avoids prepending the original dir.
    733     program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
    734     if sys.platform == 'darwin':
    735       configuration = self.configuration_dirname()
    736       os.environ['DYLD_LIBRARY_PATH'] = os.path.join('out', configuration)
    737     return self.run(program=program, *args, **kw)
    738 
    739   def built_file_path(self, name, type=None, **kw):
    740     result = []
    741     chdir = kw.get('chdir')
    742     if chdir:
    743       result.append(chdir)
    744     result.append('out')
    745     result.append(self.configuration_dirname())
    746     if type == self.STATIC_LIB:
    747       if sys.platform != 'darwin':
    748         result.append('obj')
    749     elif type == self.SHARED_LIB:
    750       if sys.platform != 'darwin' and sys.platform != 'win32':
    751         result.append('lib')
    752     subdir = kw.get('subdir')
    753     if subdir and type != self.SHARED_LIB:
    754       result.append(subdir)
    755     result.append(self.built_file_basename(name, type, **kw))
    756     return self.workpath(*result)
    757 
    758   def up_to_date(self, gyp_file, target=None, **kw):
    759     result = self.build(gyp_file, target, **kw)
    760     if not result:
    761       stdout = self.stdout()
    762       if 'ninja: no work to do' not in stdout:
    763         self.report_not_up_to_date()
    764         self.fail_test()
    765     return result
    766 
    767 
    768 class TestGypMSVS(TestGypOnMSToolchain):
    769   """
    770   Subclass for testing the GYP Visual Studio generator.
    771   """
    772   format = 'msvs'
    773 
    774   u = r'=== Build: 0 succeeded, 0 failed, (\d+) up-to-date, 0 skipped ==='
    775   up_to_date_re = re.compile(u, re.M)
    776 
    777   # Initial None element will indicate to our .initialize_build_tool()
    778   # method below that 'devenv' was not found on %PATH%.
    779   #
    780   # Note:  we must use devenv.com to be able to capture build output.
    781   # Directly executing devenv.exe only sends output to BuildLog.htm.
    782   build_tool_list = [None, 'devenv.com']
    783 
    784   def initialize_build_tool(self):
    785     super(TestGypMSVS, self).initialize_build_tool()
    786     self.build_tool = self.devenv_path
    787 
    788   def build(self, gyp_file, target=None, rebuild=False, clean=False, **kw):
    789     """
    790     Runs a Visual Studio build using the configuration generated
    791     from the specified gyp_file.
    792     """
    793     configuration = self.configuration_buildname()
    794     if clean:
    795       build = '/Clean'
    796     elif rebuild:
    797       build = '/Rebuild'
    798     else:
    799       build = '/Build'
    800     arguments = kw.get('arguments', [])[:]
    801     arguments.extend([gyp_file.replace('.gyp', '.sln'),
    802                       build, configuration])
    803     # Note:  the Visual Studio generator doesn't add an explicit 'all'
    804     # target, so we just treat it the same as the default.
    805     if target not in (None, self.ALL, self.DEFAULT):
    806       arguments.extend(['/Project', target])
    807     if self.configuration:
    808       arguments.extend(['/ProjectConfig', self.configuration])
    809     kw['arguments'] = arguments
    810     return self.run(program=self.build_tool, **kw)
    811   def up_to_date(self, gyp_file, target=None, **kw):
    812     """
    813     Verifies that a build of the specified Visual Studio target is up to date.
    814 
    815     Beware that VS2010 will behave strangely if you build under
    816     C:\USERS\yourname\AppData\Local. It will cause needless work.  The ouptut
    817     will be "1 succeeded and 0 up to date".  MSBuild tracing reveals that:
    818     "Project 'C:\Users\...\AppData\Local\...vcxproj' not up to date because
    819     'C:\PROGRAM FILES (X86)\MICROSOFT VISUAL STUDIO 10.0\VC\BIN\1033\CLUI.DLL'
    820     was modified at 02/21/2011 17:03:30, which is newer than '' which was
    821     modified at 01/01/0001 00:00:00.
    822 
    823     The workaround is to specify a workdir when instantiating the test, e.g.
    824     test = TestGyp.TestGyp(workdir='workarea')
    825     """
    826     result = self.build(gyp_file, target, **kw)
    827     if not result:
    828       stdout = self.stdout()
    829 
    830       m = self.up_to_date_re.search(stdout)
    831       up_to_date = m and int(m.group(1)) > 0
    832       if not up_to_date:
    833         self.report_not_up_to_date()
    834         self.fail_test()
    835     return result
    836   def run_built_executable(self, name, *args, **kw):
    837     """
    838     Runs an executable built by Visual Studio.
    839     """
    840     configuration = self.configuration_dirname()
    841     # Enclosing the name in a list avoids prepending the original dir.
    842     program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
    843     return self.run(program=program, *args, **kw)
    844   def built_file_path(self, name, type=None, **kw):
    845     """
    846     Returns a path to the specified file name, of the specified type,
    847     as built by Visual Studio.
    848 
    849     Built files are in a subdirectory that matches the configuration
    850     name.  The default is 'Default'.
    851 
    852     A chdir= keyword argument specifies the source directory
    853     relative to which  the output subdirectory can be found.
    854 
    855     "type" values of STATIC_LIB or SHARED_LIB append the necessary
    856     prefixes and suffixes to a platform-independent library base name.
    857     """
    858     result = []
    859     chdir = kw.get('chdir')
    860     if chdir:
    861       result.append(chdir)
    862     result.append(self.configuration_dirname())
    863     if type == self.STATIC_LIB:
    864       result.append('lib')
    865     result.append(self.built_file_basename(name, type, **kw))
    866     return self.workpath(*result)
    867 
    868 
    869 class TestGypXcode(TestGypBase):
    870   """
    871   Subclass for testing the GYP Xcode generator.
    872   """
    873   format = 'xcode'
    874   build_tool_list = ['xcodebuild']
    875 
    876   phase_script_execution = ("\n"
    877                             "PhaseScriptExecution /\\S+/Script-[0-9A-F]+\\.sh\n"
    878                             "    cd /\\S+\n"
    879                             "    /bin/sh -c /\\S+/Script-[0-9A-F]+\\.sh\n"
    880                             "(make: Nothing to be done for `all'\\.\n)?")
    881 
    882   strip_up_to_date_expressions = [
    883     # Various actions or rules can run even when the overall build target
    884     # is up to date.  Strip those phases' GYP-generated output.
    885     re.compile(phase_script_execution, re.S),
    886 
    887     # The message from distcc_pump can trail the "BUILD SUCCEEDED"
    888     # message, so strip that, too.
    889     re.compile('__________Shutting down distcc-pump include server\n', re.S),
    890   ]
    891 
    892   up_to_date_endings = (
    893     'Checking Dependencies...\n** BUILD SUCCEEDED **\n', # Xcode 3.0/3.1
    894     'Check dependencies\n** BUILD SUCCEEDED **\n\n',     # Xcode 3.2
    895     'Check dependencies\n\n\n** BUILD SUCCEEDED **\n\n', # Xcode 4.2
    896   )
    897 
    898   def build(self, gyp_file, target=None, **kw):
    899     """
    900     Runs an xcodebuild using the .xcodeproj generated from the specified
    901     gyp_file.
    902     """
    903     # Be sure we're working with a copy of 'arguments' since we modify it.
    904     # The caller may not be expecting it to be modified.
    905     arguments = kw.get('arguments', [])[:]
    906     arguments.extend(['-project', gyp_file.replace('.gyp', '.xcodeproj')])
    907     if target == self.ALL:
    908       arguments.append('-alltargets',)
    909     elif target not in (None, self.DEFAULT):
    910       arguments.extend(['-target', target])
    911     if self.configuration:
    912       arguments.extend(['-configuration', self.configuration])
    913     symroot = kw.get('SYMROOT', '$SRCROOT/build')
    914     if symroot:
    915       arguments.append('SYMROOT='+symroot)
    916     kw['arguments'] = arguments
    917 
    918     # Work around spurious stderr output from Xcode 4, http://crbug.com/181012
    919     match = kw.pop('match', self.match)
    920     def match_filter_xcode(actual, expected):
    921       if actual:
    922         if not TestCmd.is_List(actual):
    923           actual = actual.split('\n')
    924         if not TestCmd.is_List(expected):
    925           expected = expected.split('\n')
    926         actual = [a for a in actual
    927                     if 'No recorder, buildTask: <Xcode3BuildTask:' not in a]
    928       return match(actual, expected)
    929     kw['match'] = match_filter_xcode
    930 
    931     return self.run(program=self.build_tool, **kw)
    932   def up_to_date(self, gyp_file, target=None, **kw):
    933     """
    934     Verifies that a build of the specified Xcode target is up to date.
    935     """
    936     result = self.build(gyp_file, target, **kw)
    937     if not result:
    938       output = self.stdout()
    939       for expression in self.strip_up_to_date_expressions:
    940         output = expression.sub('', output)
    941       if not output.endswith(self.up_to_date_endings):
    942         self.report_not_up_to_date()
    943         self.fail_test()
    944     return result
    945   def run_built_executable(self, name, *args, **kw):
    946     """
    947     Runs an executable built by xcodebuild.
    948     """
    949     configuration = self.configuration_dirname()
    950     os.environ['DYLD_LIBRARY_PATH'] = os.path.join('build', configuration)
    951     # Enclosing the name in a list avoids prepending the original dir.
    952     program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
    953     return self.run(program=program, *args, **kw)
    954   def built_file_path(self, name, type=None, **kw):
    955     """
    956     Returns a path to the specified file name, of the specified type,
    957     as built by Xcode.
    958 
    959     Built files are in the subdirectory 'build/{configuration}'.
    960     The default is 'build/Default'.
    961 
    962     A chdir= keyword argument specifies the source directory
    963     relative to which  the output subdirectory can be found.
    964 
    965     "type" values of STATIC_LIB or SHARED_LIB append the necessary
    966     prefixes and suffixes to a platform-independent library base name.
    967     """
    968     result = []
    969     chdir = kw.get('chdir')
    970     if chdir:
    971       result.append(chdir)
    972     configuration = self.configuration_dirname()
    973     result.extend(['build', configuration])
    974     result.append(self.built_file_basename(name, type, **kw))
    975     return self.workpath(*result)
    976 
    977 
    978 format_class_list = [
    979   TestGypGypd,
    980   TestGypAndroid,
    981   TestGypMake,
    982   TestGypMSVS,
    983   TestGypNinja,
    984   TestGypXcode,
    985 ]
    986 
    987 def TestGyp(*args, **kw):
    988   """
    989   Returns an appropriate TestGyp* instance for a specified GYP format.
    990   """
    991   format = kw.pop('format', os.environ.get('TESTGYP_FORMAT'))
    992   for format_class in format_class_list:
    993     if format == format_class.format:
    994       return format_class(*args, **kw)
    995   raise Exception, "unknown format %r" % format
    996