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