1#!/usr/bin/env python
2
3# Copyright (c) 2012 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__doc__ = """
8gyptest.py -- test runner for GYP tests.
9"""
10
11import os
12import optparse
13import subprocess
14import sys
15
16class CommandRunner:
17  """
18  Executor class for commands, including "commands" implemented by
19  Python functions.
20  """
21  verbose = True
22  active = True
23
24  def __init__(self, dictionary={}):
25    self.subst_dictionary(dictionary)
26
27  def subst_dictionary(self, dictionary):
28    self._subst_dictionary = dictionary
29
30  def subst(self, string, dictionary=None):
31    """
32    Substitutes (via the format operator) the values in the specified
33    dictionary into the specified command.
34
35    The command can be an (action, string) tuple.  In all cases, we
36    perform substitution on strings and don't worry if something isn't
37    a string.  (It's probably a Python function to be executed.)
38    """
39    if dictionary is None:
40      dictionary = self._subst_dictionary
41    if dictionary:
42      try:
43        string = string % dictionary
44      except TypeError:
45        pass
46    return string
47
48  def display(self, command, stdout=None, stderr=None):
49    if not self.verbose:
50      return
51    if type(command) == type(()):
52      func = command[0]
53      args = command[1:]
54      s = '%s(%s)' % (func.__name__, ', '.join(map(repr, args)))
55    if type(command) == type([]):
56      # TODO:  quote arguments containing spaces
57      # TODO:  handle meta characters?
58      s = ' '.join(command)
59    else:
60      s = self.subst(command)
61    if not s.endswith('\n'):
62      s += '\n'
63    sys.stdout.write(s)
64    sys.stdout.flush()
65
66  def execute(self, command, stdout=None, stderr=None):
67    """
68    Executes a single command.
69    """
70    if not self.active:
71      return 0
72    if type(command) == type(''):
73      command = self.subst(command)
74      cmdargs = shlex.split(command)
75      if cmdargs[0] == 'cd':
76         command = (os.chdir,) + tuple(cmdargs[1:])
77    if type(command) == type(()):
78      func = command[0]
79      args = command[1:]
80      return func(*args)
81    else:
82      if stdout is sys.stdout:
83        # Same as passing sys.stdout, except python2.4 doesn't fail on it.
84        subout = None
85      else:
86        # Open pipe for anything else so Popen works on python2.4.
87        subout = subprocess.PIPE
88      if stderr is sys.stderr:
89        # Same as passing sys.stderr, except python2.4 doesn't fail on it.
90        suberr = None
91      elif stderr is None:
92        # Merge with stdout if stderr isn't specified.
93        suberr = subprocess.STDOUT
94      else:
95        # Open pipe for anything else so Popen works on python2.4.
96        suberr = subprocess.PIPE
97      p = subprocess.Popen(command,
98                           shell=(sys.platform == 'win32'),
99                           stdout=subout,
100                           stderr=suberr)
101      p.wait()
102      if stdout is None:
103        self.stdout = p.stdout.read()
104      elif stdout is not sys.stdout:
105        stdout.write(p.stdout.read())
106      if stderr not in (None, sys.stderr):
107        stderr.write(p.stderr.read())
108      return p.returncode
109
110  def run(self, command, display=None, stdout=None, stderr=None):
111    """
112    Runs a single command, displaying it first.
113    """
114    if display is None:
115      display = command
116    self.display(display)
117    return self.execute(command, stdout, stderr)
118
119
120class Unbuffered:
121  def __init__(self, fp):
122    self.fp = fp
123  def write(self, arg):
124    self.fp.write(arg)
125    self.fp.flush()
126  def __getattr__(self, attr):
127    return getattr(self.fp, attr)
128
129sys.stdout = Unbuffered(sys.stdout)
130sys.stderr = Unbuffered(sys.stderr)
131
132
133def find_all_gyptest_files(directory):
134    result = []
135    for root, dirs, files in os.walk(directory):
136      if '.svn' in dirs:
137        dirs.remove('.svn')
138      result.extend([ os.path.join(root, f) for f in files
139                     if f.startswith('gyptest') and f.endswith('.py') ])
140    result.sort()
141    return result
142
143
144def main(argv=None):
145  if argv is None:
146    argv = sys.argv
147
148  usage = "gyptest.py [-ahlnq] [-f formats] [test ...]"
149  parser = optparse.OptionParser(usage=usage)
150  parser.add_option("-a", "--all", action="store_true",
151            help="run all tests")
152  parser.add_option("-C", "--chdir", action="store", default=None,
153            help="chdir to the specified directory")
154  parser.add_option("-f", "--format", action="store", default='',
155            help="run tests with the specified formats")
156  parser.add_option("-G", '--gyp_option', action="append", default=[],
157            help="Add -G options to the gyp command line")
158  parser.add_option("-l", "--list", action="store_true",
159            help="list available tests and exit")
160  parser.add_option("-n", "--no-exec", action="store_true",
161            help="no execute, just print the command line")
162  parser.add_option("--passed", action="store_true",
163            help="report passed tests")
164  parser.add_option("--path", action="append", default=[],
165            help="additional $PATH directory")
166  parser.add_option("-q", "--quiet", action="store_true",
167            help="quiet, don't print test command lines")
168  opts, args = parser.parse_args(argv[1:])
169
170  if opts.chdir:
171    os.chdir(opts.chdir)
172
173  if opts.path:
174    extra_path = [os.path.abspath(p) for p in opts.path]
175    extra_path = os.pathsep.join(extra_path)
176    os.environ['PATH'] += os.pathsep + extra_path
177
178  if not args:
179    if not opts.all:
180      sys.stderr.write('Specify -a to get all tests.\n')
181      return 1
182    args = ['test']
183
184  tests = []
185  for arg in args:
186    if os.path.isdir(arg):
187      tests.extend(find_all_gyptest_files(os.path.normpath(arg)))
188    else:
189      tests.append(arg)
190
191  if opts.list:
192    for test in tests:
193      print test
194    sys.exit(0)
195
196  CommandRunner.verbose = not opts.quiet
197  CommandRunner.active = not opts.no_exec
198  cr = CommandRunner()
199
200  os.environ['PYTHONPATH'] = os.path.abspath('test/lib')
201  if not opts.quiet:
202    sys.stdout.write('PYTHONPATH=%s\n' % os.environ['PYTHONPATH'])
203
204  passed = []
205  failed = []
206  no_result = []
207
208  if opts.format:
209    format_list = opts.format.split(',')
210  else:
211    # TODO:  not duplicate this mapping from pylib/gyp/__init__.py
212    format_list = {
213      'freebsd7': ['make'],
214      'freebsd8': ['make'],
215      'openbsd5': ['make'],
216      'cygwin':   ['msvs'],
217      'win32':    ['msvs', 'ninja'],
218      'linux2':   ['make', 'ninja'],
219      'linux3':   ['make', 'ninja'],
220      'darwin':   ['make', 'ninja', 'xcode'],
221    }[sys.platform]
222
223  for format in format_list:
224    os.environ['TESTGYP_FORMAT'] = format
225    if not opts.quiet:
226      sys.stdout.write('TESTGYP_FORMAT=%s\n' % format)
227
228    gyp_options = []
229    for option in opts.gyp_option:
230      gyp_options += ['-G', option]
231    if gyp_options and not opts.quiet:
232      sys.stdout.write('Extra Gyp options: %s\n' % gyp_options)
233
234    for test in tests:
235      status = cr.run([sys.executable, test] + gyp_options,
236                      stdout=sys.stdout,
237                      stderr=sys.stderr)
238      if status == 2:
239        no_result.append(test)
240      elif status:
241        failed.append(test)
242      else:
243        passed.append(test)
244
245  if not opts.quiet:
246    def report(description, tests):
247      if tests:
248        if len(tests) == 1:
249          sys.stdout.write("\n%s the following test:\n" % description)
250        else:
251          fmt = "\n%s the following %d tests:\n"
252          sys.stdout.write(fmt % (description, len(tests)))
253        sys.stdout.write("\t" + "\n\t".join(tests) + "\n")
254
255    if opts.passed:
256      report("Passed", passed)
257    report("Failed", failed)
258    report("No result from", no_result)
259
260  if failed:
261    return 1
262  else:
263    return 0
264
265
266if __name__ == "__main__":
267  sys.exit(main())
268