1#!/usr/bin/python
2
3# Copyright (c) 2009 Google Inc. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7"""
8TestGyp.py:  a testing framework for GYP integration tests.
9"""
10
11import os
12import re
13import shutil
14import stat
15import sys
16
17import TestCommon
18from TestCommon import __all__
19
20__all__.extend([
21  'TestGyp',
22])
23
24
25class TestGypBase(TestCommon.TestCommon):
26  """
27  Class for controlling end-to-end tests of gyp generators.
28
29  Instantiating this class will create a temporary directory and
30  arrange for its destruction (via the TestCmd superclass) and
31  copy all of the non-gyptest files in the directory hierarchy of the
32  executing script.
33
34  The default behavior is to test the 'gyp' or 'gyp.bat' file in the
35  current directory.  An alternative may be specified explicitly on
36  instantiation, or by setting the TESTGYP_GYP environment variable.
37
38  This class should be subclassed for each supported gyp generator
39  (format).  Various abstract methods below define calling signatures
40  used by the test scripts to invoke builds on the generated build
41  configuration and to run executables generated by those builds.
42  """
43
44  build_tool = None
45  build_tool_list = []
46
47  _exe = TestCommon.exe_suffix
48  _obj = TestCommon.obj_suffix
49  shobj_ = TestCommon.shobj_prefix
50  _shobj = TestCommon.shobj_suffix
51  lib_ = TestCommon.lib_prefix
52  _lib = TestCommon.lib_suffix
53  dll_ = TestCommon.dll_prefix
54  _dll = TestCommon.dll_suffix
55
56  # Constants to represent different targets.
57  ALL = '__all__'
58  DEFAULT = '__default__'
59
60  # Constants for different target types.
61  EXECUTABLE = '__executable__'
62  STATIC_LIB = '__static_lib__'
63  SHARED_LIB = '__shared_lib__'
64
65  def __init__(self, gyp=None, *args, **kw):
66    self.origin_cwd = os.path.abspath(os.path.dirname(sys.argv[0]))
67
68    if not gyp:
69      gyp = os.environ.get('TESTGYP_GYP')
70      if not gyp:
71        if sys.platform == 'win32':
72          gyp = 'gyp.bat'
73        else:
74          gyp = 'gyp'
75    self.gyp = os.path.abspath(gyp)
76
77    self.initialize_build_tool()
78
79    if not kw.has_key('match'):
80      kw['match'] = TestCommon.match_exact
81
82    if not kw.has_key('workdir'):
83      # Default behavior:  the null string causes TestCmd to create
84      # a temporary directory for us.
85      kw['workdir'] = ''
86
87    formats = kw.get('formats', [])
88    if kw.has_key('formats'):
89      del kw['formats']
90
91    super(TestGypBase, self).__init__(*args, **kw)
92
93    excluded_formats = set([f for f in formats if f[0] == '!'])
94    included_formats = set(formats) - excluded_formats
95    if ('!'+self.format in excluded_formats or
96        included_formats and self.format not in included_formats):
97      msg = 'Invalid test for %r format; skipping test.\n'
98      self.skip_test(msg % self.format)
99
100    self.copy_test_configuration(self.origin_cwd, self.workdir)
101    self.set_configuration(None)
102
103  def built_file_must_exist(self, name, type=None, **kw):
104    """
105    Fails the test if the specified built file name does not exist.
106    """
107    return self.must_exist(self.built_file_path(name, type, **kw))
108
109  def built_file_must_not_exist(self, name, type=None, **kw):
110    """
111    Fails the test if the specified built file name exists.
112    """
113    return self.must_not_exist(self.built_file_path(name, type, **kw))
114
115  def built_file_must_match(self, name, contents, **kw):
116    """
117    Fails the test if the contents of the specified built file name
118    do not match the specified contents.
119    """
120    return self.must_match(self.built_file_path(name, **kw), contents)
121
122  def built_file_must_not_match(self, name, contents, **kw):
123    """
124    Fails the test if the contents of the specified built file name
125    match the specified contents.
126    """
127    return self.must_not_match(self.built_file_path(name, **kw), contents)
128
129  def copy_test_configuration(self, source_dir, dest_dir):
130    """
131    Copies the test configuration from the specified source_dir
132    (the directory in which the test script lives) to the
133    specified dest_dir (a temporary working directory).
134
135    This ignores all files and directories that begin with
136    the string 'gyptest', and all '.svn' subdirectories.
137    """
138    for root, dirs, files in os.walk(source_dir):
139      if '.svn' in dirs:
140        dirs.remove('.svn')
141      dirs = [ d for d in dirs if not d.startswith('gyptest') ]
142      files = [ f for f in files if not f.startswith('gyptest') ]
143      for dirname in dirs:
144        source = os.path.join(root, dirname)
145        destination = source.replace(source_dir, dest_dir)
146        os.mkdir(destination)
147        if sys.platform != 'win32':
148          shutil.copystat(source, destination)
149      for filename in files:
150        source = os.path.join(root, filename)
151        destination = source.replace(source_dir, dest_dir)
152        shutil.copy2(source, destination)
153
154  def initialize_build_tool(self):
155    """
156    Initializes the .build_tool attribute.
157
158    Searches the .build_tool_list for an executable name on the user's
159    $PATH.  The first tool on the list is used as-is if nothing is found
160    on the current $PATH.
161    """
162    for build_tool in self.build_tool_list:
163      if not build_tool:
164        continue
165      if os.path.isabs(build_tool):
166        self.build_tool = build_tool
167        return
168      build_tool = self.where_is(build_tool)
169      if build_tool:
170        self.build_tool = build_tool
171        return
172
173    if self.build_tool_list:
174      self.build_tool = self.build_tool_list[0]
175
176  def relocate(self, source, destination):
177    """
178    Renames (relocates) the specified source (usually a directory)
179    to the specified destination, creating the destination directory
180    first if necessary.
181
182    Note:  Don't use this as a generic "rename" operation.  In the
183    future, "relocating" parts of a GYP tree may affect the state of
184    the test to modify the behavior of later method calls.
185    """
186    destination_dir = os.path.dirname(destination)
187    if not os.path.exists(destination_dir):
188      self.subdir(destination_dir)
189    os.rename(source, destination)
190
191  def report_not_up_to_date(self):
192    """
193    Reports that a build is not up-to-date.
194
195    This provides common reporting for formats that have complicated
196    conditions for checking whether a build is up-to-date.  Formats
197    that expect exact output from the command (make, scons) can
198    just set stdout= when they call the run_build() method.
199    """
200    print "Build is not up-to-date:"
201    print self.banner('STDOUT ')
202    print self.stdout()
203    stderr = self.stderr()
204    if stderr:
205      print self.banner('STDERR ')
206      print stderr
207
208  def run_gyp(self, gyp_file, *args, **kw):
209    """
210    Runs gyp against the specified gyp_file with the specified args.
211    """
212    # TODO:  --depth=. works around Chromium-specific tree climbing.
213    args = ('--depth=.', '--format='+self.format, gyp_file) + args
214    return self.run(program=self.gyp, arguments=args, **kw)
215
216  def run(self, *args, **kw):
217    """
218    Executes a program by calling the superclass .run() method.
219
220    This exists to provide a common place to filter out keyword
221    arguments implemented in this layer, without having to update
222    the tool-specific subclasses or clutter the tests themselves
223    with platform-specific code.
224    """
225    if kw.has_key('SYMROOT'):
226      del kw['SYMROOT']
227    super(TestGypBase, self).run(*args, **kw)
228
229  def set_configuration(self, configuration):
230    """
231    Sets the configuration, to be used for invoking the build
232    tool and testing potential built output.
233    """
234    self.configuration = configuration
235
236  def configuration_dirname(self):
237    if self.configuration:
238      return self.configuration.split('|')[0]
239    else:
240      return 'Default'
241
242  def configuration_buildname(self):
243    if self.configuration:
244      return self.configuration
245    else:
246      return 'Default'
247
248  #
249  # Abstract methods to be defined by format-specific subclasses.
250  #
251
252  def build(self, gyp_file, target=None, **kw):
253    """
254    Runs a build of the specified target against the configuration
255    generated from the specified gyp_file.
256
257    A 'target' argument of None or the special value TestGyp.DEFAULT
258    specifies the default argument for the underlying build tool.
259    A 'target' argument of TestGyp.ALL specifies the 'all' target
260    (if any) of the underlying build tool.
261    """
262    raise NotImplementedError
263
264  def built_file_path(self, name, type=None, **kw):
265    """
266    Returns a path to the specified file name, of the specified type.
267    """
268    raise NotImplementedError
269
270  def built_file_basename(self, name, type=None, **kw):
271    """
272    Returns the base name of the specified file name, of the specified type.
273
274    A bare=True keyword argument specifies that prefixes and suffixes shouldn't
275    be applied.
276    """
277    if not kw.get('bare'):
278      if type == self.EXECUTABLE:
279        name = name + self._exe
280      elif type == self.STATIC_LIB:
281        name = self.lib_ + name + self._lib
282      elif type == self.SHARED_LIB:
283        name = self.dll_ + name + self._dll
284    return name
285
286  def run_built_executable(self, name, *args, **kw):
287    """
288    Runs an executable program built from a gyp-generated configuration.
289
290    The specified name should be independent of any particular generator.
291    Subclasses should find the output executable in the appropriate
292    output build directory, tack on any necessary executable suffix, etc.
293    """
294    raise NotImplementedError
295
296  def up_to_date(self, gyp_file, target=None, **kw):
297    """
298    Verifies that a build of the specified target is up to date.
299
300    The subclass should implement this by calling build()
301    (or a reasonable equivalent), checking whatever conditions
302    will tell it the build was an "up to date" null build, and
303    failing if it isn't.
304    """
305    raise NotImplementedError
306
307
308class TestGypGypd(TestGypBase):
309  """
310  Subclass for testing the GYP 'gypd' generator (spit out the
311  internal data structure as pretty-printed Python).
312  """
313  format = 'gypd'
314
315
316class TestGypMake(TestGypBase):
317  """
318  Subclass for testing the GYP Make generator.
319  """
320  format = 'make'
321  build_tool_list = ['make']
322  ALL = 'all'
323  def build(self, gyp_file, target=None, **kw):
324    """
325    Runs a Make build using the Makefiles generated from the specified
326    gyp_file.
327    """
328    arguments = kw.get('arguments', [])[:]
329    if self.configuration:
330      arguments.append('BUILDTYPE=' + self.configuration)
331    if target not in (None, self.DEFAULT):
332      arguments.append(target)
333    # Sub-directory builds provide per-gyp Makefiles (i.e.
334    # Makefile.gyp_filename), so use that if there is no Makefile.
335    chdir = kw.get('chdir', '')
336    if not os.path.exists(os.path.join(chdir, 'Makefile')):
337      print "NO Makefile in " + os.path.join(chdir, 'Makefile')
338      arguments.insert(0, '-f')
339      arguments.insert(1, os.path.splitext(gyp_file)[0] + '.Makefile')
340    kw['arguments'] = arguments
341    return self.run(program=self.build_tool, **kw)
342  def up_to_date(self, gyp_file, target=None, **kw):
343    """
344    Verifies that a build of the specified Make target is up to date.
345    """
346    if target in (None, self.DEFAULT):
347      message_target = 'all'
348    else:
349      message_target = target
350    kw['stdout'] = "make: Nothing to be done for `%s'.\n" % message_target
351    return self.build(gyp_file, target, **kw)
352  def run_built_executable(self, name, *args, **kw):
353    """
354    Runs an executable built by Make.
355    """
356    configuration = self.configuration_dirname()
357    libdir = os.path.join('out', configuration, 'lib')
358    # TODO(piman): when everything is cross-compile safe, remove lib.target
359    os.environ['LD_LIBRARY_PATH'] = libdir + '.host:' + libdir + '.target'
360    # Enclosing the name in a list avoids prepending the original dir.
361    program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
362    return self.run(program=program, *args, **kw)
363  def built_file_path(self, name, type=None, **kw):
364    """
365    Returns a path to the specified file name, of the specified type,
366    as built by Make.
367
368    Built files are in the subdirectory 'out/{configuration}'.
369    The default is 'out/Default'.
370
371    A chdir= keyword argument specifies the source directory
372    relative to which  the output subdirectory can be found.
373
374    "type" values of STATIC_LIB or SHARED_LIB append the necessary
375    prefixes and suffixes to a platform-independent library base name.
376
377    A libdir= keyword argument specifies a library subdirectory other
378    than the default 'obj.target'.
379    """
380    result = []
381    chdir = kw.get('chdir')
382    if chdir:
383      result.append(chdir)
384    configuration = self.configuration_dirname()
385    result.extend(['out', configuration])
386    if type == self.STATIC_LIB:
387      result.append(kw.get('libdir', 'obj.target'))
388    elif type == self.SHARED_LIB:
389      result.append(kw.get('libdir', 'lib.target'))
390    result.append(self.built_file_basename(name, type, **kw))
391    return self.workpath(*result)
392
393
394class TestGypMSVS(TestGypBase):
395  """
396  Subclass for testing the GYP Visual Studio generator.
397  """
398  format = 'msvs'
399
400  u = r'=== Build: (\d+) succeeded, 0 failed, (\d+) up-to-date, 0 skipped ==='
401  up_to_date_re = re.compile(u, re.M)
402
403  # Initial None element will indicate to our .initialize_build_tool()
404  # method below that 'devenv' was not found on %PATH%.
405  #
406  # Note:  we must use devenv.com to be able to capture build output.
407  # Directly executing devenv.exe only sends output to BuildLog.htm.
408  build_tool_list = [None, 'devenv.com']
409
410  def initialize_build_tool(self):
411    """ Initializes the Visual Studio .build_tool and .uses_msbuild parameters.
412
413    We use the value specified by GYP_MSVS_VERSION.  If not specified, we
414    search %PATH% and %PATHEXT% for a devenv.{exe,bat,...} executable.
415    Failing that, we search for likely deployment paths.
416    """
417    super(TestGypMSVS, self).initialize_build_tool()
418    possible_roots = ['C:\\Program Files (x86)', 'C:\\Program Files']
419    possible_paths = {
420        '2010': r'Microsoft Visual Studio 10.0\Common7\IDE\devenv.com',
421        '2008': r'Microsoft Visual Studio 9.0\Common7\IDE\devenv.com',
422        '2005': r'Microsoft Visual Studio 8\Common7\IDE\devenv.com'}
423    msvs_version = os.environ.get('GYP_MSVS_VERSION', 'auto')
424    if msvs_version in possible_paths:
425      # Check that the path to the specified GYP_MSVS_VERSION exists.
426      path = possible_paths[msvs_version]
427      for r in possible_roots:
428        bt = os.path.join(r, path)
429        if os.path.exists(bt):
430          self.build_tool = bt
431          self.uses_msbuild = msvs_version >= '2010'
432          return
433      else:
434        print ('Warning: Environment variable GYP_MSVS_VERSION specifies "%s" '
435               'but corresponding "%s" was not found.' % (msvs_version, path))
436    if self.build_tool:
437      # We found 'devenv' on the path, use that and try to guess the version.
438      for version, path in possible_paths.iteritems():
439        if self.build_tool.find(path) >= 0:
440          self.uses_msbuild = version >= '2010'
441          return
442      else:
443        # If not, assume not MSBuild.
444        self.uses_msbuild = False
445      return
446    # Neither GYP_MSVS_VERSION nor the path help us out.  Iterate through
447    # the choices looking for a match.
448    for version, path in possible_paths.iteritems():
449      for r in possible_roots:
450        bt = os.path.join(r, path)
451        if os.path.exists(bt):
452          self.build_tool = bt
453          self.uses_msbuild = msvs_version >= '2010'
454          return
455    print 'Error: could not find devenv'
456    sys.exit(1)
457  def build(self, gyp_file, target=None, rebuild=False, **kw):
458    """
459    Runs a Visual Studio build using the configuration generated
460    from the specified gyp_file.
461    """
462    configuration = self.configuration_buildname()
463    if rebuild:
464      build = '/Rebuild'
465    else:
466      build = '/Build'
467    arguments = kw.get('arguments', [])[:]
468    arguments.extend([gyp_file.replace('.gyp', '.sln'),
469                      build, configuration])
470    # Note:  the Visual Studio generator doesn't add an explicit 'all'
471    # target, so we just treat it the same as the default.
472    if target not in (None, self.ALL, self.DEFAULT):
473      arguments.extend(['/Project', target])
474    if self.configuration:
475      arguments.extend(['/ProjectConfig', self.configuration])
476    kw['arguments'] = arguments
477    return self.run(program=self.build_tool, **kw)
478  def up_to_date(self, gyp_file, target=None, **kw):
479    """
480    Verifies that a build of the specified Visual Studio target is up to date.
481    """
482    result = self.build(gyp_file, target, **kw)
483    if not result:
484      stdout = self.stdout()
485      m = self.up_to_date_re.search(stdout)
486      up_to_date = False
487      if m:
488        succeeded = m.group(1)
489        up_to_date = m.group(2)
490        up_to_date = succeeded == '0' and up_to_date == '1'
491        # Figuring out if the build is up to date changed with VS2010.
492        # For builds that should be up to date, I sometimes get
493        # "1 succeeded and 0 up to date".  As an ad-hoc measure, we check
494        # this and also verify that th number of output lines is small.
495        # I don't know if this is caused by VS itself or is due to
496        # interaction with virus checkers.
497        if self.uses_msbuild and (succeeded == '1' and
498             up_to_date == '0' and
499             stdout.count('\n') <= 6):
500          up_to_date = True
501      if not up_to_date:
502        self.report_not_up_to_date()
503        self.fail_test()
504    return result
505  def run_built_executable(self, name, *args, **kw):
506    """
507    Runs an executable built by Visual Studio.
508    """
509    configuration = self.configuration_dirname()
510    # Enclosing the name in a list avoids prepending the original dir.
511    program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
512    return self.run(program=program, *args, **kw)
513  def built_file_path(self, name, type=None, **kw):
514    """
515    Returns a path to the specified file name, of the specified type,
516    as built by Visual Studio.
517
518    Built files are in a subdirectory that matches the configuration
519    name.  The default is 'Default'.
520
521    A chdir= keyword argument specifies the source directory
522    relative to which  the output subdirectory can be found.
523
524    "type" values of STATIC_LIB or SHARED_LIB append the necessary
525    prefixes and suffixes to a platform-independent library base name.
526    """
527    result = []
528    chdir = kw.get('chdir')
529    if chdir:
530      result.append(chdir)
531    result.append(self.configuration_dirname())
532    if type == self.STATIC_LIB:
533      result.append('lib')
534    result.append(self.built_file_basename(name, type, **kw))
535    return self.workpath(*result)
536
537
538class TestGypSCons(TestGypBase):
539  """
540  Subclass for testing the GYP SCons generator.
541  """
542  format = 'scons'
543  build_tool_list = ['scons', 'scons.py']
544  ALL = 'all'
545  def build(self, gyp_file, target=None, **kw):
546    """
547    Runs a scons build using the SCons configuration generated from the
548    specified gyp_file.
549    """
550    arguments = kw.get('arguments', [])[:]
551    dirname = os.path.dirname(gyp_file)
552    if dirname:
553      arguments.extend(['-C', dirname])
554    if self.configuration:
555      arguments.append('--mode=' + self.configuration)
556    if target not in (None, self.DEFAULT):
557      arguments.append(target)
558    kw['arguments'] = arguments
559    return self.run(program=self.build_tool, **kw)
560  def up_to_date(self, gyp_file, target=None, **kw):
561    """
562    Verifies that a build of the specified SCons target is up to date.
563    """
564    if target in (None, self.DEFAULT):
565      up_to_date_targets = 'all'
566    else:
567      up_to_date_targets = target
568    up_to_date_lines = []
569    for arg in up_to_date_targets.split():
570      up_to_date_lines.append("scons: `%s' is up to date.\n" % arg)
571    kw['stdout'] = ''.join(up_to_date_lines)
572    arguments = kw.get('arguments', [])[:]
573    arguments.append('-Q')
574    kw['arguments'] = arguments
575    return self.build(gyp_file, target, **kw)
576  def run_built_executable(self, name, *args, **kw):
577    """
578    Runs an executable built by scons.
579    """
580    configuration = self.configuration_dirname()
581    os.environ['LD_LIBRARY_PATH'] = os.path.join(configuration, 'lib')
582    # Enclosing the name in a list avoids prepending the original dir.
583    program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
584    return self.run(program=program, *args, **kw)
585  def built_file_path(self, name, type=None, **kw):
586    """
587    Returns a path to the specified file name, of the specified type,
588    as built by Scons.
589
590    Built files are in a subdirectory that matches the configuration
591    name.  The default is 'Default'.
592
593    A chdir= keyword argument specifies the source directory
594    relative to which  the output subdirectory can be found.
595
596    "type" values of STATIC_LIB or SHARED_LIB append the necessary
597    prefixes and suffixes to a platform-independent library base name.
598    """
599    result = []
600    chdir = kw.get('chdir')
601    if chdir:
602      result.append(chdir)
603    result.append(self.configuration_dirname())
604    if type in (self.STATIC_LIB, self.SHARED_LIB):
605      result.append('lib')
606    result.append(self.built_file_basename(name, type, **kw))
607    return self.workpath(*result)
608
609
610class TestGypXcode(TestGypBase):
611  """
612  Subclass for testing the GYP Xcode generator.
613  """
614  format = 'xcode'
615  build_tool_list = ['xcodebuild']
616
617  phase_script_execution = ("\n"
618                            "PhaseScriptExecution /\\S+/Script-[0-9A-F]+\\.sh\n"
619                            "    cd /\\S+\n"
620                            "    /bin/sh -c /\\S+/Script-[0-9A-F]+\\.sh\n"
621                            "(make: Nothing to be done for `all'\\.\n)?")
622
623  strip_up_to_date_expressions = [
624    # Various actions or rules can run even when the overall build target
625    # is up to date.  Strip those phases' GYP-generated output.
626    re.compile(phase_script_execution, re.S),
627
628    # The message from distcc_pump can trail the "BUILD SUCCEEDED"
629    # message, so strip that, too.
630    re.compile('__________Shutting down distcc-pump include server\n', re.S),
631  ]
632
633  up_to_date_endings = (
634    'Checking Dependencies...\n** BUILD SUCCEEDED **\n', # Xcode 3.0/3.1
635    'Check dependencies\n** BUILD SUCCEEDED **\n\n',     # Xcode 3.2
636  )
637
638  def build(self, gyp_file, target=None, **kw):
639    """
640    Runs an xcodebuild using the .xcodeproj generated from the specified
641    gyp_file.
642    """
643    # Be sure we're working with a copy of 'arguments' since we modify it.
644    # The caller may not be expecting it to be modified.
645    arguments = kw.get('arguments', [])[:]
646    arguments.extend(['-project', gyp_file.replace('.gyp', '.xcodeproj')])
647    if target == self.ALL:
648      arguments.append('-alltargets',)
649    elif target not in (None, self.DEFAULT):
650      arguments.extend(['-target', target])
651    if self.configuration:
652      arguments.extend(['-configuration', self.configuration])
653    symroot = kw.get('SYMROOT', '$SRCROOT/build')
654    if symroot:
655      arguments.append('SYMROOT='+symroot)
656    kw['arguments'] = arguments
657    return self.run(program=self.build_tool, **kw)
658  def up_to_date(self, gyp_file, target=None, **kw):
659    """
660    Verifies that a build of the specified Xcode target is up to date.
661    """
662    result = self.build(gyp_file, target, **kw)
663    if not result:
664      output = self.stdout()
665      for expression in self.strip_up_to_date_expressions:
666        output = expression.sub('', output)
667      if not output.endswith(self.up_to_date_endings):
668        self.report_not_up_to_date()
669        self.fail_test()
670    return result
671  def run_built_executable(self, name, *args, **kw):
672    """
673    Runs an executable built by xcodebuild.
674    """
675    configuration = self.configuration_dirname()
676    os.environ['DYLD_LIBRARY_PATH'] = os.path.join('build', configuration)
677    # Enclosing the name in a list avoids prepending the original dir.
678    program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)]
679    return self.run(program=program, *args, **kw)
680  def built_file_path(self, name, type=None, **kw):
681    """
682    Returns a path to the specified file name, of the specified type,
683    as built by Xcode.
684
685    Built files are in the subdirectory 'build/{configuration}'.
686    The default is 'build/Default'.
687
688    A chdir= keyword argument specifies the source directory
689    relative to which  the output subdirectory can be found.
690
691    "type" values of STATIC_LIB or SHARED_LIB append the necessary
692    prefixes and suffixes to a platform-independent library base name.
693    """
694    result = []
695    chdir = kw.get('chdir')
696    if chdir:
697      result.append(chdir)
698    configuration = self.configuration_dirname()
699    result.extend(['build', configuration])
700    result.append(self.built_file_basename(name, type, **kw))
701    return self.workpath(*result)
702
703
704format_class_list = [
705  TestGypGypd,
706  TestGypMake,
707  TestGypMSVS,
708  TestGypSCons,
709  TestGypXcode,
710]
711
712def TestGyp(*args, **kw):
713  """
714  Returns an appropriate TestGyp* instance for a specified GYP format.
715  """
716  format = kw.get('format')
717  if format:
718    del kw['format']
719  else:
720    format = os.environ.get('TESTGYP_FORMAT')
721  for format_class in format_class_list:
722    if format == format_class.format:
723      return format_class(*args, **kw)
724  raise Exception, "unknown format %r" % format
725