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