From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- third_party/python/gyp/test/lib/README.txt | 17 + third_party/python/gyp/test/lib/TestCmd.py | 1597 +++++++++++++++++++++++++ third_party/python/gyp/test/lib/TestCommon.py | 591 +++++++++ third_party/python/gyp/test/lib/TestGyp.py | 1260 +++++++++++++++++++ third_party/python/gyp/test/lib/TestMac.py | 76 ++ third_party/python/gyp/test/lib/TestWin.py | 101 ++ 6 files changed, 3642 insertions(+) create mode 100644 third_party/python/gyp/test/lib/README.txt create mode 100644 third_party/python/gyp/test/lib/TestCmd.py create mode 100644 third_party/python/gyp/test/lib/TestCommon.py create mode 100644 third_party/python/gyp/test/lib/TestGyp.py create mode 100644 third_party/python/gyp/test/lib/TestMac.py create mode 100644 third_party/python/gyp/test/lib/TestWin.py (limited to 'third_party/python/gyp/test/lib') diff --git a/third_party/python/gyp/test/lib/README.txt b/third_party/python/gyp/test/lib/README.txt new file mode 100644 index 0000000000..b3d724574e --- /dev/null +++ b/third_party/python/gyp/test/lib/README.txt @@ -0,0 +1,17 @@ +Supporting modules for GYP testing. + + TestCmd.py + TestCommon.py + + Modules for generic testing of command-line utilities, + specifically including the ability to copy a test configuration + to temporary directories (with default cleanup on exit) as part + of running test scripts that invoke commands, compare actual + against expected output, etc. + + Our copies of these come from the SCons project, + http://www.scons.org/. + + TestGyp.py + + Modules for GYP-specific tests, of course. diff --git a/third_party/python/gyp/test/lib/TestCmd.py b/third_party/python/gyp/test/lib/TestCmd.py new file mode 100644 index 0000000000..1ec50933a4 --- /dev/null +++ b/third_party/python/gyp/test/lib/TestCmd.py @@ -0,0 +1,1597 @@ +# Copyright (c) 2018 Google Inc. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +""" +TestCmd.py: a testing framework for commands and scripts. + +The TestCmd module provides a framework for portable automated testing +of executable commands and scripts (in any language, not just Python), +especially commands and scripts that require file system interaction. + +In addition to running tests and evaluating conditions, the TestCmd +module manages and cleans up one or more temporary workspace +directories, and provides methods for creating files and directories in +those workspace directories from in-line data, here-documents), allowing +tests to be completely self-contained. + +A TestCmd environment object is created via the usual invocation: + + import TestCmd + test = TestCmd.TestCmd() + +There are a bunch of keyword arguments available at instantiation: + + test = TestCmd.TestCmd(description = 'string', + program = 'program_or_script_to_test', + interpreter = 'script_interpreter', + workdir = 'prefix', + subdir = 'subdir', + verbose = Boolean, + match = default_match_function, + diff = default_diff_function, + combine = Boolean) + +There are a bunch of methods that let you do different things: + + test.verbose_set(1) + + test.description_set('string') + + test.program_set('program_or_script_to_test') + + test.interpreter_set('script_interpreter') + test.interpreter_set(['script_interpreter', 'arg']) + + test.workdir_set('prefix') + test.workdir_set('') + + test.workpath('file') + test.workpath('subdir', 'file') + + test.subdir('subdir', ...) + + test.rmdir('subdir', ...) + + test.write('file', "contents\n") + test.write(['subdir', 'file'], "contents\n") + + test.read('file') + test.read(['subdir', 'file']) + test.read('file', mode) + test.read(['subdir', 'file'], mode) + + test.writable('dir', 1) + test.writable('dir', None) + + test.preserve(condition, ...) + + test.cleanup(condition) + + test.command_args(program = 'program_or_script_to_run', + interpreter = 'script_interpreter', + arguments = 'arguments to pass to program') + + test.run(program = 'program_or_script_to_run', + interpreter = 'script_interpreter', + arguments = 'arguments to pass to program', + chdir = 'directory_to_chdir_to', + stdin = 'input to feed to the program\n') + universal_newlines = True) + + p = test.start(program = 'program_or_script_to_run', + interpreter = 'script_interpreter', + arguments = 'arguments to pass to program', + universal_newlines = None) + + test.finish(self, p) + + test.pass_test() + test.pass_test(condition) + test.pass_test(condition, function) + + test.fail_test() + test.fail_test(condition) + test.fail_test(condition, function) + test.fail_test(condition, function, skip) + + test.no_result() + test.no_result(condition) + test.no_result(condition, function) + test.no_result(condition, function, skip) + + test.stdout() + test.stdout(run) + + test.stderr() + test.stderr(run) + + test.symlink(target, link) + + test.banner(string) + test.banner(string, width) + + test.diff(actual, expected) + + test.match(actual, expected) + + test.match_exact("actual 1\nactual 2\n", "expected 1\nexpected 2\n") + test.match_exact(["actual 1\n", "actual 2\n"], + ["expected 1\n", "expected 2\n"]) + + test.match_re("actual 1\nactual 2\n", regex_string) + test.match_re(["actual 1\n", "actual 2\n"], list_of_regexes) + + test.match_re_dotall("actual 1\nactual 2\n", regex_string) + test.match_re_dotall(["actual 1\n", "actual 2\n"], list_of_regexes) + + test.tempdir() + test.tempdir('temporary-directory') + + test.sleep() + test.sleep(seconds) + + test.where_is('foo') + test.where_is('foo', 'PATH1:PATH2') + test.where_is('foo', 'PATH1;PATH2', '.suffix3;.suffix4') + + test.unlink('file') + test.unlink('subdir', 'file') + +The TestCmd module provides pass_test(), fail_test(), and no_result() +unbound functions that report test results for use with the Aegis change +management system. These methods terminate the test immediately, +reporting PASSED, FAILED, or NO RESULT respectively, and exiting with +status 0 (success), 1 or 2 respectively. This allows for a distinction +between an actual failed test and a test that could not be properly +evaluated because of an external condition (such as a full file system +or incorrect permissions). + + import TestCmd + + TestCmd.pass_test() + TestCmd.pass_test(condition) + TestCmd.pass_test(condition, function) + + TestCmd.fail_test() + TestCmd.fail_test(condition) + TestCmd.fail_test(condition, function) + TestCmd.fail_test(condition, function, skip) + + TestCmd.no_result() + TestCmd.no_result(condition) + TestCmd.no_result(condition, function) + TestCmd.no_result(condition, function, skip) + +The TestCmd module also provides unbound functions that handle matching +in the same way as the match_*() methods described above. + + import TestCmd + + test = TestCmd.TestCmd(match = TestCmd.match_exact) + + test = TestCmd.TestCmd(match = TestCmd.match_re) + + test = TestCmd.TestCmd(match = TestCmd.match_re_dotall) + +The TestCmd module provides unbound functions that can be used for the +"diff" argument to TestCmd.TestCmd instantiation: + + import TestCmd + + test = TestCmd.TestCmd(match = TestCmd.match_re, + diff = TestCmd.diff_re) + + test = TestCmd.TestCmd(diff = TestCmd.simple_diff) + +The "diff" argument can also be used with standard difflib functions: + + import difflib + + test = TestCmd.TestCmd(diff = difflib.context_diff) + + test = TestCmd.TestCmd(diff = difflib.unified_diff) + +Lastly, the where_is() method also exists in an unbound function +version. + + import TestCmd + + TestCmd.where_is('foo') + TestCmd.where_is('foo', 'PATH1:PATH2') + TestCmd.where_is('foo', 'PATH1;PATH2', '.suffix3;.suffix4') +""" + +# Copyright 2000-2010 Steven Knight +# This module is free software, and you may redistribute it and/or modify +# it under the same terms as Python itself, so long as this copyright message +# and disclaimer are retained in their original form. +# +# IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, +# SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF +# THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +# DAMAGE. +# +# THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +# PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, +# AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, +# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + +from __future__ import print_function + +__author__ = "Steven Knight " +__revision__ = "TestCmd.py 0.37.D001 2010/01/11 16:55:50 knight" +__version__ = "0.37" + +import errno +import os +import os.path +import re +import shutil +import stat +import sys +import tempfile +import time +import traceback +try: + from UserList import UserList +except ImportError: + from collections import UserList + +__all__ = [ + 'diff_re', + 'fail_test', + 'no_result', + 'pass_test', + 'match_exact', + 'match_re', + 'match_re_dotall', + 'python_executable', + 'TestCmd' +] + +try: + import difflib +except ImportError: + __all__.append('simple_diff') + +def is_List(e): + return (type(e) is list) or isinstance(e, UserList) + +try: + from UserString import UserString +except ImportError: + try: + from collections import UserString + except ImportError: + class UserString: + pass + +try: + # basestring was removed in python3. + basestring +except NameError: + basestring = str + +def is_String(e): + return isinstance(e, basestring) or isinstance(e, UserString) + +tempfile.template = 'testcmd.' +if os.name in ('posix', 'nt'): + tempfile.template = 'testcmd.' + str(os.getpid()) + '.' +else: + tempfile.template = 'testcmd.' + +re_space = re.compile('\s') + +_Cleanup = [] + +_chain_to_exitfunc = None + +def _clean(): + global _Cleanup + for test in reversed(_Cleanup): + if test: + test.cleanup() + del _Cleanup[:] + if _chain_to_exitfunc: + _chain_to_exitfunc() + +try: + import atexit +except ImportError: + # TODO(1.5): atexit requires python 2.0, so chain sys.exitfunc + try: + _chain_to_exitfunc = sys.exitfunc + except AttributeError: + pass + sys.exitfunc = _clean +else: + atexit.register(_clean) + +try: + zip +except NameError: + def zip(*lists): + result = [] + for i in range(min(map(len, lists))): + result.append(tuple(map(lambda l, i=i: l[i], lists))) + return result + +class Collector: + def __init__(self, top): + self.entries = [top] + def __call__(self, arg, dirname, names): + pathjoin = lambda n, d=dirname: os.path.join(d, n) + self.entries.extend(map(pathjoin, names)) + +def _caller(tblist, skip): + string = "" + arr = [] + for file, line, name, text in tblist: + if file[-10:] == "TestCmd.py": + break + arr = [(file, line, name, text)] + arr + atfrom = "at" + for file, line, name, text in arr[skip:]: + if name in ("?", ""): + name = "" + else: + name = " (" + name + ")" + string = string + ("%s line %d of %s%s\n" % (atfrom, line, file, name)) + atfrom = "\tfrom" + return string + +def fail_test(self = None, condition = 1, function = None, skip = 0): + """Cause the test to fail. + + By default, the fail_test() method reports that the test FAILED + and exits with a status of 1. If a condition argument is supplied, + the test fails only if the condition is true. + """ + if not condition: + return + if not function is None: + function() + of = "" + desc = "" + sep = " " + if not self is None: + if self.program: + of = " of " + self.program + sep = "\n\t" + if self.description: + desc = " [" + self.description + "]" + sep = "\n\t" + + at = _caller(traceback.extract_stack(), skip) + sys.stderr.write("FAILED test" + of + desc + sep + at) + + sys.exit(1) + +def no_result(self = None, condition = 1, function = None, skip = 0): + """Causes a test to exit with no valid result. + + By default, the no_result() method reports NO RESULT for the test + and exits with a status of 2. If a condition argument is supplied, + the test fails only if the condition is true. + """ + if not condition: + return + if not function is None: + function() + of = "" + desc = "" + sep = " " + if not self is None: + if self.program: + of = " of " + self.program + sep = "\n\t" + if self.description: + desc = " [" + self.description + "]" + sep = "\n\t" + + if os.environ.get('TESTCMD_DEBUG_SKIPS'): + at = _caller(traceback.extract_stack(), skip) + sys.stderr.write("NO RESULT for test" + of + desc + sep + at) + else: + sys.stderr.write("NO RESULT\n") + + sys.exit(2) + +def pass_test(self = None, condition = 1, function = None): + """Causes a test to pass. + + By default, the pass_test() method reports PASSED for the test + and exits with a status of 0. If a condition argument is supplied, + the test passes only if the condition is true. + """ + if not condition: + return + if not function is None: + function() + sys.stderr.write("PASSED\n") + sys.exit(0) + +def match_exact(lines = None, matches = None): + """ + """ + if not is_List(lines): + lines = lines.split("\n") + if not is_List(matches): + matches = matches.split("\n") + if len(lines) != len(matches): + return + for i in range(len(lines)): + if lines[i] != matches[i]: + return + return 1 + +def match_re(lines = None, res = None): + """ + """ + if not is_List(lines): + lines = lines.split("\n") + if not is_List(res): + res = res.split("\n") + if len(lines) != len(res): + return + for i in range(len(lines)): + s = "^" + res[i] + "$" + try: + expr = re.compile(s) + except re.error as e: + msg = "Regular expression error in %s: %s" + raise re.error(msg % (repr(s), e[0])) + if not expr.search(lines[i]): + return + return 1 + +def match_re_dotall(lines = None, res = None): + """ + """ + if not type(lines) is type(""): + lines = "\n".join(lines) + if not type(res) is type(""): + res = "\n".join(res) + s = "^" + res + "$" + try: + expr = re.compile(s, re.DOTALL) + except re.error as e: + msg = "Regular expression error in %s: %s" + raise re.error(msg % (repr(s), e[0])) + if expr.match(lines): + return 1 + +try: + import difflib +except ImportError: + pass +else: + def simple_diff(a, b, fromfile='', tofile='', + fromfiledate='', tofiledate='', n=3, lineterm='\n'): + """ + A function with the same calling signature as difflib.context_diff + (diff -c) and difflib.unified_diff (diff -u) but which prints + output like the simple, unadorned 'diff" command. + """ + sm = difflib.SequenceMatcher(None, a, b) + def comma(x1, x2): + return x1+1 == x2 and str(x2) or '%s,%s' % (x1+1, x2) + result = [] + for op, a1, a2, b1, b2 in sm.get_opcodes(): + if op == 'delete': + result.append("%sd%d" % (comma(a1, a2), b1)) + result.extend(map(lambda l: '< ' + l, a[a1:a2])) + elif op == 'insert': + result.append("%da%s" % (a1, comma(b1, b2))) + result.extend(map(lambda l: '> ' + l, b[b1:b2])) + elif op == 'replace': + result.append("%sc%s" % (comma(a1, a2), comma(b1, b2))) + result.extend(map(lambda l: '< ' + l, a[a1:a2])) + result.append('---') + result.extend(map(lambda l: '> ' + l, b[b1:b2])) + return result + +def diff_re(a, b, fromfile='', tofile='', + fromfiledate='', tofiledate='', n=3, lineterm='\n'): + """ + A simple "diff" of two sets of lines when the expected lines + are regular expressions. This is a really dumb thing that + just compares each line in turn, so it doesn't look for + chunks of matching lines and the like--but at least it lets + you know exactly which line first didn't compare correctl... + """ + result = [] + diff = len(a) - len(b) + if diff < 0: + a = a + ['']*(-diff) + elif diff > 0: + b = b + ['']*diff + i = 0 + for aline, bline in zip(a, b): + s = "^" + aline + "$" + try: + expr = re.compile(s) + except re.error as e: + msg = "Regular expression error in %s: %s" + raise re.error(msg % (repr(s), e[0])) + if not expr.search(bline): + result.append("%sc%s" % (i+1, i+1)) + result.append('< ' + repr(a[i])) + result.append('---') + result.append('> ' + repr(b[i])) + i = i+1 + return result + +if os.name == 'java': + + python_executable = os.path.join(sys.prefix, 'jython') + +else: + + python_executable = sys.executable + +if sys.platform == 'win32': + + default_sleep_seconds = 2 + + def where_is(file, path=None, pathext=None): + if path is None: + path = os.environ['PATH'] + if is_String(path): + path = path.split(os.pathsep) + if pathext is None: + pathext = os.environ['PATHEXT'] + if is_String(pathext): + pathext = pathext.split(os.pathsep) + for ext in pathext: + if ext.lower() == file[-len(ext):].lower(): + pathext = [''] + break + for dir in path: + f = os.path.join(dir, file) + for ext in pathext: + fext = f + ext + if os.path.isfile(fext): + return fext + return None + +else: + + def where_is(file, path=None, pathext=None): + if path is None: + path = os.environ['PATH'] + if is_String(path): + path = path.split(os.pathsep) + for dir in path: + f = os.path.join(dir, file) + if os.path.isfile(f): + try: + st = os.stat(f) + except OSError: + continue + if stat.S_IMODE(st[stat.ST_MODE]) & 0o111: + return f + return None + + default_sleep_seconds = 1 + + + +try: + import subprocess +except ImportError: + # The subprocess module doesn't exist in this version of Python, + # so we're going to cobble up something that looks just enough + # like its API for our purposes below. + import new + + subprocess = new.module('subprocess') + + subprocess.PIPE = 'PIPE' + subprocess.STDOUT = 'STDOUT' + subprocess.mswindows = (sys.platform == 'win32') + + try: + import popen2 + popen2.Popen3 + except AttributeError: + class Popen3: + universal_newlines = 1 + def __init__(self, command, **kw): + if sys.platform == 'win32' and command[0] == '"': + command = '"' + command + '"' + (stdin, stdout, stderr) = os.popen3(' ' + command) + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + def close_output(self): + self.stdout.close() + self.resultcode = self.stderr.close() + def wait(self): + resultcode = self.resultcode + if os.WIFEXITED(resultcode): + return os.WEXITSTATUS(resultcode) + elif os.WIFSIGNALED(resultcode): + return os.WTERMSIG(resultcode) + else: + return None + + else: + try: + popen2.Popen4 + except AttributeError: + # A cribbed Popen4 class, with some retrofitted code from + # the Python 1.5 Popen3 class methods to do certain things + # by hand. + class Popen4(popen2.Popen3): + childerr = None + + def __init__(self, cmd, bufsize=-1): + p2cread, p2cwrite = os.pipe() + c2pread, c2pwrite = os.pipe() + self.pid = os.fork() + if self.pid == 0: + # Child + os.dup2(p2cread, 0) + os.dup2(c2pwrite, 1) + os.dup2(c2pwrite, 2) + for i in range(3, popen2.MAXFD): + try: + os.close(i) + except: pass + try: + os.execvp(cmd[0], cmd) + finally: + os._exit(1) + # Shouldn't come here, I guess + os._exit(1) + os.close(p2cread) + self.tochild = os.fdopen(p2cwrite, 'w', bufsize) + os.close(c2pwrite) + self.fromchild = os.fdopen(c2pread, 'r', bufsize) + popen2._active.append(self) + + popen2.Popen4 = Popen4 + + class Popen3(popen2.Popen3, popen2.Popen4): + universal_newlines = 1 + def __init__(self, command, **kw): + if kw.get('stderr') == 'STDOUT': + popen2.Popen4.__init__(self, command, 1) + else: + popen2.Popen3.__init__(self, command, 1) + self.stdin = self.tochild + self.stdout = self.fromchild + self.stderr = self.childerr + def wait(self, *args, **kw): + resultcode = popen2.Popen3.wait(self, *args, **kw) + if os.WIFEXITED(resultcode): + return os.WEXITSTATUS(resultcode) + elif os.WIFSIGNALED(resultcode): + return os.WTERMSIG(resultcode) + else: + return None + + subprocess.Popen = Popen3 + + + +# From Josiah Carlson, +# ASPN : Python Cookbook : Module to allow Asynchronous subprocess use on Windows and Posix platforms +# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/440554 + +PIPE = subprocess.PIPE + +if sys.platform == 'win32': + from win32file import ReadFile, WriteFile + from win32pipe import PeekNamedPipe + import msvcrt +else: + import select + import fcntl + + try: fcntl.F_GETFL + except AttributeError: fcntl.F_GETFL = 3 + + try: fcntl.F_SETFL + except AttributeError: fcntl.F_SETFL = 4 + +class Popen(subprocess.Popen): + def recv(self, maxsize=None): + return self._recv('stdout', maxsize) + + def recv_err(self, maxsize=None): + return self._recv('stderr', maxsize) + + def send_recv(self, input='', maxsize=None): + return self.send(input), self.recv(maxsize), self.recv_err(maxsize) + + def get_conn_maxsize(self, which, maxsize): + if maxsize is None: + maxsize = 1024 + elif maxsize < 1: + maxsize = 1 + return getattr(self, which), maxsize + + def _close(self, which): + getattr(self, which).close() + setattr(self, which, None) + + if sys.platform == 'win32': + def send(self, input): + if not self.stdin: + return None + + try: + x = msvcrt.get_osfhandle(self.stdin.fileno()) + (errCode, written) = WriteFile(x, input) + except ValueError: + return self._close('stdin') + except (subprocess.pywintypes.error, Exception) as why: + if why[0] in (109, errno.ESHUTDOWN): + return self._close('stdin') + raise + + return written + + def _recv(self, which, maxsize): + conn, maxsize = self.get_conn_maxsize(which, maxsize) + if conn is None: + return None + + try: + x = msvcrt.get_osfhandle(conn.fileno()) + (read, nAvail, nMessage) = PeekNamedPipe(x, 0) + if maxsize < nAvail: + nAvail = maxsize + if nAvail > 0: + (errCode, read) = ReadFile(x, nAvail, None) + except ValueError: + return self._close(which) + except (subprocess.pywintypes.error, Exception) as why: + if why[0] in (109, errno.ESHUTDOWN): + return self._close(which) + raise + + #if self.universal_newlines: + # read = self._translate_newlines(read) + return read + + else: + def send(self, input): + if not self.stdin: + return None + + if not select.select([], [self.stdin], [], 0)[1]: + return 0 + + try: + written = os.write(self.stdin.fileno(), input) + except OSError as why: + if why[0] == errno.EPIPE: #broken pipe + return self._close('stdin') + raise + + return written + + def _recv(self, which, maxsize): + conn, maxsize = self.get_conn_maxsize(which, maxsize) + if conn is None: + return None + + try: + flags = fcntl.fcntl(conn, fcntl.F_GETFL) + except TypeError: + flags = None + else: + if not conn.closed: + fcntl.fcntl(conn, fcntl.F_SETFL, flags| os.O_NONBLOCK) + + try: + if not select.select([conn], [], [], 0)[0]: + return '' + + r = conn.read(maxsize) + if not r: + return self._close(which) + + #if self.universal_newlines: + # r = self._translate_newlines(r) + return r + finally: + if not conn.closed and not flags is None: + fcntl.fcntl(conn, fcntl.F_SETFL, flags) + +disconnect_message = "Other end disconnected!" + +def recv_some(p, t=.1, e=1, tr=5, stderr=0): + if tr < 1: + tr = 1 + x = time.time()+t + y = [] + r = '' + pr = p.recv + if stderr: + pr = p.recv_err + while time.time() < x or r: + r = pr() + if r is None: + if e: + raise Exception(disconnect_message) + else: + break + elif r: + y.append(r) + else: + time.sleep(max((x-time.time())/tr, 0)) + return ''.join(y) + +def send_all(p, data): + data = memoryview(data) + while len(data): + sent = p.send(data) + if sent is None: + raise Exception(disconnect_message) + data = data[sent:] + + + +class TestCmd(object): + """Class TestCmd + """ + + def __init__(self, description = None, + program = None, + interpreter = None, + workdir = None, + subdir = None, + verbose = None, + match = None, + diff = None, + combine = 0, + universal_newlines = 1): + self._cwd = os.getcwd() + self.description_set(description) + self.program_set(program) + self.interpreter_set(interpreter) + if verbose is None: + try: + verbose = max( 0, int(os.environ.get('TESTCMD_VERBOSE', 0)) ) + except ValueError: + verbose = 0 + self.verbose_set(verbose) + self.combine = combine + self.universal_newlines = universal_newlines + if match is not None: + self.match_function = match + else: + self.match_function = match_re + if diff is not None: + self.diff_function = diff + else: + try: + difflib + except NameError: + pass + else: + self.diff_function = simple_diff + #self.diff_function = difflib.context_diff + #self.diff_function = difflib.unified_diff + self._dirlist = [] + self._preserve = {'pass_test': 0, 'fail_test': 0, 'no_result': 0} + if 'PRESERVE' in os.environ and os.environ['PRESERVE'] is not '': + self._preserve['pass_test'] = os.environ['PRESERVE'] + self._preserve['fail_test'] = os.environ['PRESERVE'] + self._preserve['no_result'] = os.environ['PRESERVE'] + else: + try: + self._preserve['pass_test'] = os.environ['PRESERVE_PASS'] + except KeyError: + pass + try: + self._preserve['fail_test'] = os.environ['PRESERVE_FAIL'] + except KeyError: + pass + try: + self._preserve['no_result'] = os.environ['PRESERVE_NO_RESULT'] + except KeyError: + pass + self._stdout = [] + self._stderr = [] + self.status = None + self.condition = 'no_result' + self.workdir_set(workdir) + self.subdir(subdir) + + def __del__(self): + self.cleanup() + + def __repr__(self): + return "%x" % id(self) + + banner_char = '=' + banner_width = 80 + + def banner(self, s, width=None): + if width is None: + width = self.banner_width + return s + self.banner_char * (width - len(s)) + + if os.name == 'posix': + + def escape(self, arg): + "escape shell special characters" + slash = '\\' + special = '"$' + + arg = arg.replace(slash, slash+slash) + for c in special: + arg = arg.replace(c, slash+c) + + if re_space.search(arg): + arg = '"' + arg + '"' + return arg + + else: + + # Windows does not allow special characters in file names + # anyway, so no need for an escape function, we will just quote + # the arg. + def escape(self, arg): + if re_space.search(arg): + arg = '"' + arg + '"' + return arg + + def canonicalize(self, path): + if is_List(path): + path = os.path.join(*path) + if not os.path.isabs(path): + path = os.path.join(self.workdir, path) + return path + + def chmod(self, path, mode): + """Changes permissions on the specified file or directory + path name.""" + path = self.canonicalize(path) + os.chmod(path, mode) + + def cleanup(self, condition = None): + """Removes any temporary working directories for the specified + TestCmd environment. If the environment variable PRESERVE was + set when the TestCmd environment was created, temporary working + directories are not removed. If any of the environment variables + PRESERVE_PASS, PRESERVE_FAIL, or PRESERVE_NO_RESULT were set + when the TestCmd environment was created, then temporary working + directories are not removed if the test passed, failed, or had + no result, respectively. Temporary working directories are also + preserved for conditions specified via the preserve method. + + Typically, this method is not called directly, but is used when + the script exits to clean up temporary working directories as + appropriate for the exit status. + """ + if not self._dirlist: + return + os.chdir(self._cwd) + self.workdir = None + if condition is None: + condition = self.condition + if self._preserve[condition]: + for dir in self._dirlist: + print("Preserved directory", dir) + else: + list = self._dirlist[:] + list.reverse() + for dir in list: + self.writable(dir, 1) + shutil.rmtree(dir, ignore_errors = 1) + self._dirlist = [] + + try: + global _Cleanup + _Cleanup.remove(self) + except (AttributeError, ValueError): + pass + + def command_args(self, program = None, + interpreter = None, + arguments = None): + if program: + if type(program) == type('') and not os.path.isabs(program): + program = os.path.join(self._cwd, program) + else: + program = self.program + if not interpreter: + interpreter = self.interpreter + if not type(program) in [type([]), type(())]: + program = [program] + cmd = list(program) + if interpreter: + if not type(interpreter) in [type([]), type(())]: + interpreter = [interpreter] + cmd = list(interpreter) + cmd + if arguments: + if type(arguments) == type(''): + arguments = arguments.split() + cmd.extend(arguments) + return cmd + + def description_set(self, description): + """Set the description of the functionality being tested. + """ + self.description = description + + try: + difflib + except NameError: + def diff(self, a, b, name, *args, **kw): + print(self.banner('Expected %s' % name)) + print(a) + print(self.banner('Actual %s' % name)) + print(b) + else: + def diff(self, a, b, name, *args, **kw): + print(self.banner(name)) + args = (a.splitlines(), b.splitlines()) + args + lines = self.diff_function(*args, **kw) + for l in lines: + print(l) + + def fail_test(self, condition = 1, function = None, skip = 0): + """Cause the test to fail. + """ + if not condition: + return + self.condition = 'fail_test' + fail_test(self = self, + condition = condition, + function = function, + skip = skip) + + def interpreter_set(self, interpreter): + """Set the program to be used to interpret the program + under test as a script. + """ + self.interpreter = interpreter + + def match(self, lines, matches): + """Compare actual and expected file contents. + """ + return self.match_function(lines, matches) + + def match_exact(self, lines, matches): + """Compare actual and expected file contents. + """ + return match_exact(lines, matches) + + def match_re(self, lines, res): + """Compare actual and expected file contents. + """ + return match_re(lines, res) + + def match_re_dotall(self, lines, res): + """Compare actual and expected file contents. + """ + return match_re_dotall(lines, res) + + def no_result(self, condition = 1, function = None, skip = 0): + """Report that the test could not be run. + """ + if not condition: + return + self.condition = 'no_result' + no_result(self = self, + condition = condition, + function = function, + skip = skip) + + def pass_test(self, condition = 1, function = None): + """Cause the test to pass. + """ + if not condition: + return + self.condition = 'pass_test' + pass_test(self = self, condition = condition, function = function) + + def preserve(self, *conditions): + """Arrange for the temporary working directories for the + specified TestCmd environment to be preserved for one or more + conditions. If no conditions are specified, arranges for + the temporary working directories to be preserved for all + conditions. + """ + if conditions is (): + conditions = ('pass_test', 'fail_test', 'no_result') + for cond in conditions: + self._preserve[cond] = 1 + + def program_set(self, program): + """Set the executable program or script to be tested. + """ + if program and not os.path.isabs(program): + program = os.path.join(self._cwd, program) + self.program = program + + def read(self, file, mode = 'r'): + """Reads and returns the contents of the specified file name. + The file name may be a list, in which case the elements are + concatenated with the os.path.join() method. The file is + assumed to be under the temporary working directory unless it + is an absolute path name. The I/O mode for the file may + be specified; it must begin with an 'r'. The default is + 'r' (string read). + """ + file = self.canonicalize(file) + if mode[0] != 'r': + raise ValueError("mode must begin with 'r'") + with open(file, mode) as f: + result = f.read() + return result + + def rmdir(self, dir): + """Removes the specified dir name. + The dir name may be a list, in which case the elements are + concatenated with the os.path.join() method. The dir is + assumed to be under the temporary working directory unless it + is an absolute path name. + The dir must be empty. + """ + dir = self.canonicalize(dir) + os.rmdir(dir) + + def start(self, program = None, + interpreter = None, + arguments = None, + universal_newlines = None, + **kw): + """ + Starts a program or script for the test environment. + + The specified program will have the original directory + prepended unless it is enclosed in a [list]. + """ + cmd = self.command_args(program, interpreter, arguments) + cmd_string = ' '.join(map(self.escape, cmd)) + if self.verbose: + sys.stderr.write(cmd_string + "\n") + if universal_newlines is None: + universal_newlines = self.universal_newlines + + # On Windows, if we make stdin a pipe when we plan to send + # no input, and the test program exits before + # Popen calls msvcrt.open_osfhandle, that call will fail. + # So don't use a pipe for stdin if we don't need one. + stdin = kw.get('stdin', None) + if stdin is not None: + stdin = subprocess.PIPE + + combine = kw.get('combine', self.combine) + if combine: + stderr_value = subprocess.STDOUT + else: + stderr_value = subprocess.PIPE + + return Popen(cmd, + stdin=stdin, + stdout=subprocess.PIPE, + stderr=stderr_value, + universal_newlines=universal_newlines) + + def finish(self, popen, **kw): + """ + Finishes and waits for the process being run under control of + the specified popen argument, recording the exit status, + standard output and error output. + """ + popen.stdin.close() + self.status = popen.wait() + if not self.status: + self.status = 0 + self._stdout.append(popen.stdout.read()) + if popen.stderr: + stderr = popen.stderr.read() + else: + stderr = '' + self._stderr.append(stderr) + + def run(self, program = None, + interpreter = None, + arguments = None, + chdir = None, + stdin = None, + universal_newlines = None): + """Runs a test of the program or script for the test + environment. Standard output and error output are saved for + future retrieval via the stdout() and stderr() methods. + + The specified program will have the original directory + prepended unless it is enclosed in a [list]. + """ + if chdir: + oldcwd = os.getcwd() + if not os.path.isabs(chdir): + chdir = os.path.join(self.workpath(chdir)) + if self.verbose: + sys.stderr.write("chdir(" + chdir + ")\n") + os.chdir(chdir) + p = self.start(program, + interpreter, + arguments, + universal_newlines, + stdin=stdin) + if stdin: + if is_List(stdin): + for line in stdin: + p.stdin.write(line) + else: + p.stdin.write(stdin) + p.stdin.close() + + out = p.stdout.read() + if p.stderr is None: + err = '' + else: + err = p.stderr.read() + try: + close_output = p.close_output + except AttributeError: + p.stdout.close() + if not p.stderr is None: + p.stderr.close() + else: + close_output() + + self._stdout.append(out) + self._stderr.append(err) + + self.status = p.wait() + if not self.status: + self.status = 0 + + if chdir: + os.chdir(oldcwd) + if self.verbose >= 2: + write = sys.stdout.write + write('============ STATUS: %d\n' % self.status) + out = self.stdout() + if out or self.verbose >= 3: + write('============ BEGIN STDOUT (len=%d):\n' % len(out)) + write(out) + write('============ END STDOUT\n') + err = self.stderr() + if err or self.verbose >= 3: + write('============ BEGIN STDERR (len=%d)\n' % len(err)) + write(err) + write('============ END STDERR\n') + + def sleep(self, seconds = default_sleep_seconds): + """Sleeps at least the specified number of seconds. If no + number is specified, sleeps at least the minimum number of + seconds necessary to advance file time stamps on the current + system. Sleeping more seconds is all right. + """ + time.sleep(seconds) + + def stderr(self, run = None): + """Returns the error output from the specified run number. + If there is no specified run number, then returns the error + output of the last run. If the run number is less than zero, + then returns the error output from that many runs back from the + current run. + """ + if not run: + run = len(self._stderr) + elif run < 0: + run = len(self._stderr) + run + run = run - 1 + return self._stderr[run] + + def stdout(self, run = None): + """Returns the standard output from the specified run number. + If there is no specified run number, then returns the standard + output of the last run. If the run number is less than zero, + then returns the standard output from that many runs back from + the current run. + """ + if not run: + run = len(self._stdout) + elif run < 0: + run = len(self._stdout) + run + run = run - 1 + return self._stdout[run] + + def subdir(self, *subdirs): + """Create new subdirectories under the temporary working + directory, one for each argument. An argument may be a list, + in which case the list elements are concatenated using the + os.path.join() method. Subdirectories multiple levels deep + must be created using a separate argument for each level: + + test.subdir('sub', ['sub', 'dir'], ['sub', 'dir', 'ectory']) + + Returns the number of subdirectories actually created. + """ + count = 0 + for sub in subdirs: + if sub is None: + continue + if is_List(sub): + sub = os.path.join(*sub) + new = os.path.join(self.workdir, sub) + try: + os.mkdir(new) + except OSError: + pass + else: + count = count + 1 + return count + + def symlink(self, target, link): + """Creates a symlink to the specified target. + The link name may be a list, in which case the elements are + concatenated with the os.path.join() method. The link is + assumed to be under the temporary working directory unless it + is an absolute path name. The target is *not* assumed to be + under the temporary working directory. + """ + link = self.canonicalize(link) + os.symlink(target, link) + + def tempdir(self, path=None): + """Creates a temporary directory. + A unique directory name is generated if no path name is specified. + The directory is created, and will be removed when the TestCmd + object is destroyed. + """ + if path is None: + try: + path = tempfile.mktemp(prefix=tempfile.template) + except TypeError: + path = tempfile.mktemp() + os.mkdir(path) + + # Symlinks in the path will report things + # differently from os.getcwd(), so chdir there + # and back to fetch the canonical path. + cwd = os.getcwd() + try: + os.chdir(path) + path = os.getcwd() + finally: + os.chdir(cwd) + + # Uppercase the drive letter since the case of drive + # letters is pretty much random on win32: + drive,rest = os.path.splitdrive(path) + if drive: + path = drive.upper() + rest + + # + self._dirlist.append(path) + global _Cleanup + try: + _Cleanup.index(self) + except ValueError: + _Cleanup.append(self) + + return path + + def touch(self, path, mtime=None): + """Updates the modification time on the specified file or + directory path name. The default is to update to the + current time if no explicit modification time is specified. + """ + path = self.canonicalize(path) + atime = os.path.getatime(path) + if mtime is None: + mtime = time.time() + os.utime(path, (atime, mtime)) + + def unlink(self, file): + """Unlinks the specified file name. + The file name may be a list, in which case the elements are + concatenated with the os.path.join() method. The file is + assumed to be under the temporary working directory unless it + is an absolute path name. + """ + file = self.canonicalize(file) + os.unlink(file) + + def verbose_set(self, verbose): + """Set the verbose level. + """ + self.verbose = verbose + + def where_is(self, file, path=None, pathext=None): + """Find an executable file. + """ + if is_List(file): + file = os.path.join(*file) + if not os.path.isabs(file): + file = where_is(file, path, pathext) + return file + + def workdir_set(self, path): + """Creates a temporary working directory with the specified + path name. If the path is a null string (''), a unique + directory name is created. + """ + if (path != None): + if path == '': + path = None + path = self.tempdir(path) + self.workdir = path + + def workpath(self, *args): + """Returns the absolute path name to a subdirectory or file + within the current temporary working directory. Concatenates + the temporary working directory name with the specified + arguments using the os.path.join() method. + """ + return os.path.join(self.workdir, *args) + + def readable(self, top, read=1): + """Make the specified directory tree readable (read == 1) + or not (read == None). + + This method has no effect on Windows systems, which use a + completely different mechanism to control file readability. + """ + + if sys.platform == 'win32': + return + + if read: + def do_chmod(fname): + try: st = os.stat(fname) + except OSError: pass + else: os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE]|stat.S_IREAD)) + else: + def do_chmod(fname): + try: st = os.stat(fname) + except OSError: pass + else: os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE]&~stat.S_IREAD)) + + if os.path.isfile(top): + # If it's a file, that's easy, just chmod it. + do_chmod(top) + elif read: + # It's a directory and we're trying to turn on read + # permission, so it's also pretty easy, just chmod the + # directory and then chmod every entry on our walk down the + # tree. Because os.walk() is top-down, we'll enable + # read permission on any directories that have it disabled + # before os.walk() tries to list their contents. + do_chmod(top) + + def chmod_entries(arg, dirname, names, do_chmod=do_chmod): + for n in names: + do_chmod(os.path.join(dirname, n)) + + os.walk(top, chmod_entries, None) + else: + # It's a directory and we're trying to turn off read + # permission, which means we have to chmod the directoreis + # in the tree bottom-up, lest disabling read permission from + # the top down get in the way of being able to get at lower + # parts of the tree. But os.walk() visits things top + # down, so we just use an object to collect a list of all + # of the entries in the tree, reverse the list, and then + # chmod the reversed (bottom-up) list. + col = Collector(top) + os.walk(top, col, None) + col.entries.reverse() + for d in col.entries: do_chmod(d) + + def writable(self, top, write=1): + """Make the specified directory tree writable (write == 1) + or not (write == None). + """ + + if sys.platform == 'win32': + + if write: + def do_chmod(fname): + try: os.chmod(fname, stat.S_IWRITE) + except OSError: pass + else: + def do_chmod(fname): + try: os.chmod(fname, stat.S_IREAD) + except OSError: pass + + else: + + if write: + def do_chmod(fname): + try: st = os.stat(fname) + except OSError: pass + else: os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE]|0o200)) + else: + def do_chmod(fname): + try: st = os.stat(fname) + except OSError: pass + else: os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE]&~0o200)) + + if os.path.isfile(top): + do_chmod(top) + else: + col = Collector(top) + os.walk(top, col, None) + for d in col.entries: do_chmod(d) + + def executable(self, top, execute=1): + """Make the specified directory tree executable (execute == 1) + or not (execute == None). + + This method has no effect on Windows systems, which use a + completely different mechanism to control file executability. + """ + + if sys.platform == 'win32': + return + + if execute: + def do_chmod(fname): + try: st = os.stat(fname) + except OSError: pass + else: os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE]|stat.S_IEXEC)) + else: + def do_chmod(fname): + try: st = os.stat(fname) + except OSError: pass + else: os.chmod(fname, stat.S_IMODE(st[stat.ST_MODE]&~stat.S_IEXEC)) + + if os.path.isfile(top): + # If it's a file, that's easy, just chmod it. + do_chmod(top) + elif execute: + # It's a directory and we're trying to turn on execute + # permission, so it's also pretty easy, just chmod the + # directory and then chmod every entry on our walk down the + # tree. Because os.walk() is top-down, we'll enable + # execute permission on any directories that have it disabled + # before os.walk() tries to list their contents. + do_chmod(top) + + def chmod_entries(arg, dirname, names, do_chmod=do_chmod): + for n in names: + do_chmod(os.path.join(dirname, n)) + + os.walk(top, chmod_entries, None) + else: + # It's a directory and we're trying to turn off execute + # permission, which means we have to chmod the directories + # in the tree bottom-up, lest disabling execute permission from + # the top down get in the way of being able to get at lower + # parts of the tree. But os.walk() visits things top + # down, so we just use an object to collect a list of all + # of the entries in the tree, reverse the list, and then + # chmod the reversed (bottom-up) list. + col = Collector(top) + os.walk(top, col, None) + col.entries.reverse() + for d in col.entries: do_chmod(d) + + def write(self, file, content, mode = 'w'): + """Writes the specified content text (second argument) to the + specified file name (first argument). The file name may be + a list, in which case the elements are concatenated with the + os.path.join() method. The file is created under the temporary + working directory. Any subdirectories in the path must already + exist. The I/O mode for the file may be specified; it must + begin with a 'w'. The default is 'w' (binary write). + """ + file = self.canonicalize(file) + if mode[0] != 'w': + raise ValueError("mode must begin with 'w'") + with open(file, mode) as f: + f.write(content) + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/third_party/python/gyp/test/lib/TestCommon.py b/third_party/python/gyp/test/lib/TestCommon.py new file mode 100644 index 0000000000..6850ce9ada --- /dev/null +++ b/third_party/python/gyp/test/lib/TestCommon.py @@ -0,0 +1,591 @@ +# Copyright (c) 2017 Google Inc. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +""" +TestCommon.py: a testing framework for commands and scripts + with commonly useful error handling + +The TestCommon module provides a simple, high-level interface for writing +tests of executable commands and scripts, especially commands and scripts +that interact with the file system. All methods throw exceptions and +exit on failure, with useful error messages. This makes a number of +explicit checks unnecessary, making the test scripts themselves simpler +to write and easier to read. + +The TestCommon class is a subclass of the TestCmd class. In essence, +TestCommon is a wrapper that handles common TestCmd error conditions in +useful ways. You can use TestCommon directly, or subclass it for your +program and add additional (or override) methods to tailor it to your +program's specific needs. Alternatively, the TestCommon class serves +as a useful example of how to define your own TestCmd subclass. + +As a subclass of TestCmd, TestCommon provides access to all of the +variables and methods from the TestCmd module. Consequently, you can +use any variable or method documented in the TestCmd module without +having to explicitly import TestCmd. + +A TestCommon environment object is created via the usual invocation: + + import TestCommon + test = TestCommon.TestCommon() + +You can use all of the TestCmd keyword arguments when instantiating a +TestCommon object; see the TestCmd documentation for details. + +Here is an overview of the methods and keyword arguments that are +provided by the TestCommon class: + + test.must_be_writable('file1', ['file2', ...]) + + test.must_contain('file', 'required text\n') + + test.must_contain_all_lines(output, lines, ['title', find]) + + test.must_contain_any_line(output, lines, ['title', find]) + + test.must_exist('file1', ['file2', ...]) + + test.must_match('file', "expected contents\n") + + test.must_not_be_writable('file1', ['file2', ...]) + + test.must_not_contain('file', 'banned text\n') + + test.must_not_contain_any_line(output, lines, ['title', find]) + + test.must_not_exist('file1', ['file2', ...]) + + test.run(options = "options to be prepended to arguments", + stdout = "expected standard output from the program", + stderr = "expected error output from the program", + status = expected_status, + match = match_function) + +The TestCommon module also provides the following variables + + TestCommon.python_executable + TestCommon.exe_suffix + TestCommon.obj_suffix + TestCommon.shobj_prefix + TestCommon.shobj_suffix + TestCommon.lib_prefix + TestCommon.lib_suffix + TestCommon.dll_prefix + TestCommon.dll_suffix + +""" + +# Copyright 2000-2010 Steven Knight +# This module is free software, and you may redistribute it and/or modify +# it under the same terms as Python itself, so long as this copyright message +# and disclaimer are retained in their original form. +# +# IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, +# SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF +# THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +# DAMAGE. +# +# THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +# PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, +# AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, +# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. + +from __future__ import print_function + +__author__ = "Steven Knight " +__revision__ = "TestCommon.py 0.37.D001 2010/01/11 16:55:50 knight" +__version__ = "0.37" + +import copy +import os +import os.path +import stat +import sys +try: + from UserList import UserList +except ImportError: + from collections import UserList + +from TestCmd import * +from TestCmd import __all__ + +__all__.extend([ 'TestCommon', + 'exe_suffix', + 'obj_suffix', + 'shobj_prefix', + 'shobj_suffix', + 'lib_prefix', + 'lib_suffix', + 'dll_prefix', + 'dll_suffix', + ]) + +# Variables that describe the prefixes and suffixes on this system. +if sys.platform == 'win32': + exe_suffix = '.exe' + obj_suffix = '.obj' + shobj_suffix = '.obj' + shobj_prefix = '' + lib_prefix = '' + lib_suffix = '.lib' + dll_prefix = '' + dll_suffix = '.dll' + module_prefix = '' + module_suffix = '.dll' +elif sys.platform == 'cygwin': + exe_suffix = '.exe' + obj_suffix = '.o' + shobj_suffix = '.os' + shobj_prefix = '' + lib_prefix = 'lib' + lib_suffix = '.a' + dll_prefix = '' + dll_suffix = '.dll' + module_prefix = '' + module_suffix = '.dll' +elif sys.platform.find('irix') != -1: + exe_suffix = '' + obj_suffix = '.o' + shobj_suffix = '.o' + shobj_prefix = '' + lib_prefix = 'lib' + lib_suffix = '.a' + dll_prefix = 'lib' + dll_suffix = '.so' + module_prefix = 'lib' + module_prefix = '.so' +elif sys.platform.find('darwin') != -1: + exe_suffix = '' + obj_suffix = '.o' + shobj_suffix = '.os' + shobj_prefix = '' + lib_prefix = 'lib' + lib_suffix = '.a' + dll_prefix = 'lib' + dll_suffix = '.dylib' + module_prefix = '' + module_suffix = '.so' +elif sys.platform.find('sunos') != -1: + exe_suffix = '' + obj_suffix = '.o' + shobj_suffix = '.os' + shobj_prefix = 'so_' + lib_prefix = 'lib' + lib_suffix = '.a' + dll_prefix = 'lib' + dll_suffix = '.dylib' + module_prefix = '' + module_suffix = '.so' +else: + exe_suffix = '' + obj_suffix = '.o' + shobj_suffix = '.os' + shobj_prefix = '' + lib_prefix = 'lib' + lib_suffix = '.a' + dll_prefix = 'lib' + dll_suffix = '.so' + module_prefix = 'lib' + module_suffix = '.so' + +def is_List(e): + return type(e) is list \ + or isinstance(e, UserList) + +def is_writable(f): + mode = os.stat(f)[stat.ST_MODE] + return mode & stat.S_IWUSR + +def separate_files(flist): + existing = [] + missing = [] + for f in flist: + if os.path.exists(f): + existing.append(f) + else: + missing.append(f) + return existing, missing + +def _failed(self, status = 0): + if self.status is None or status is None: + return None + try: + return _status(self) not in status + except TypeError: + # status wasn't an iterable + return _status(self) != status + +def _status(self): + return self.status + +class TestCommon(TestCmd): + + # Additional methods from the Perl Test::Cmd::Common module + # that we may wish to add in the future: + # + # $test->subdir('subdir', ...); + # + # $test->copy('src_file', 'dst_file'); + + def __init__(self, **kw): + """Initialize a new TestCommon instance. This involves just + calling the base class initialization, and then changing directory + to the workdir. + """ + TestCmd.__init__(self, **kw) + os.chdir(self.workdir) + + def must_be_writable(self, *files): + """Ensures that the specified file(s) exist and are writable. + An individual file can be specified as a list of directory names, + in which case the pathname will be constructed by concatenating + them. Exits FAILED if any of the files does not exist or is + not writable. + """ + files = map((lambda x: os.path.join(*x) if is_List(x) else x), files) + existing, missing = separate_files(files) + unwritable = [x for x in existing if not is_writable(x)] + if missing: + print("Missing files: `%s'" % "', `".join(missing)) + if unwritable: + print("Unwritable files: `%s'" % "', `".join(unwritable)) + self.fail_test(missing + unwritable) + + def must_contain(self, file, required, mode = 'r'): + """Ensures that the specified file contains the required text. + """ + file_contents = self.read(file, mode) + contains = (file_contents.find(required) != -1) + if not contains: + print("File `%s' does not contain required string." % file) + print(self.banner('Required string ')) + print(required) + print(self.banner('%s contents ' % file)) + print(file_contents) + self.fail_test(not contains) + + def must_contain_all_lines(self, output, lines, title=None, find=None): + """Ensures that the specified output string (first argument) + contains all of the specified lines (second argument). + + An optional third argument can be used to describe the type + of output being searched, and only shows up in failure output. + + An optional fourth argument can be used to supply a different + function, of the form "find(line, output), to use when searching + for lines in the output. + """ + if find is None: + find = lambda o, l: o.find(l) != -1 + missing = [] + for line in lines: + if not find(output, line): + missing.append(line) + + if missing: + if title is None: + title = 'output' + sys.stdout.write("Missing expected lines from %s:\n" % title) + for line in missing: + sys.stdout.write(' ' + repr(line) + '\n') + sys.stdout.write(self.banner(title + ' ')) + sys.stdout.write(output) + self.fail_test() + + def must_contain_any_line(self, output, lines, title=None, find=None): + """Ensures that the specified output string (first argument) + contains at least one of the specified lines (second argument). + + An optional third argument can be used to describe the type + of output being searched, and only shows up in failure output. + + An optional fourth argument can be used to supply a different + function, of the form "find(line, output), to use when searching + for lines in the output. + """ + if find is None: + find = lambda o, l: o.find(l) != -1 + for line in lines: + if find(output, line): + return + + if title is None: + title = 'output' + sys.stdout.write("Missing any expected line from %s:\n" % title) + for line in lines: + sys.stdout.write(' ' + repr(line) + '\n') + sys.stdout.write(self.banner(title + ' ')) + sys.stdout.write(output) + self.fail_test() + + def must_contain_lines(self, lines, output, title=None): + # Deprecated; retain for backwards compatibility. + return self.must_contain_all_lines(output, lines, title) + + def must_exist(self, *files): + """Ensures that the specified file(s) must exist. An individual + file be specified as a list of directory names, in which case the + pathname will be constructed by concatenating them. Exits FAILED + if any of the files does not exist. + """ + files = map((lambda x: os.path.join(*x) if is_List(x) else x), files) + missing = [f for f in files if not os.path.exists(f)] + if missing: + print("Missing files: `%s'" % "', `".join(missing)) + self.fail_test(missing) + + def must_match(self, file, expect, mode = 'r'): + """Matches the contents of the specified file (first argument) + against the expected contents (second argument). The expected + contents are a list of lines or a string which will be split + on newlines. + """ + file_contents = self.read(file, mode) + try: + self.fail_test(not self.match(file_contents, expect)) + except KeyboardInterrupt: + raise + except: + print("Unexpected contents of `%s'" % file) + self.diff(expect, file_contents, 'contents ') + raise + + def must_not_contain(self, file, banned, mode = 'r'): + """Ensures that the specified file doesn't contain the banned text. + """ + file_contents = self.read(file, mode) + contains = (file_contents.find(banned) != -1) + if contains: + print("File `%s' contains banned string." % file) + print(self.banner('Banned string ')) + print(banned) + print(self.banner('%s contents ' % file)) + print(file_contents) + self.fail_test(contains) + + def must_not_contain_any_line(self, output, lines, title=None, find=None): + """Ensures that the specified output string (first argument) + does not contain any of the specified lines (second argument). + + An optional third argument can be used to describe the type + of output being searched, and only shows up in failure output. + + An optional fourth argument can be used to supply a different + function, of the form "find(line, output), to use when searching + for lines in the output. + """ + if find is None: + find = lambda o, l: o.find(l) != -1 + unexpected = [] + for line in lines: + if find(output, line): + unexpected.append(line) + + if unexpected: + if title is None: + title = 'output' + sys.stdout.write("Unexpected lines in %s:\n" % title) + for line in unexpected: + sys.stdout.write(' ' + repr(line) + '\n') + sys.stdout.write(self.banner(title + ' ')) + sys.stdout.write(output) + self.fail_test() + + def must_not_contain_lines(self, lines, output, title=None): + return self.must_not_contain_any_line(output, lines, title) + + def must_not_exist(self, *files): + """Ensures that the specified file(s) must not exist. + An individual file be specified as a list of directory names, in + which case the pathname will be constructed by concatenating them. + Exits FAILED if any of the files exists. + """ + files = map((lambda x: os.path.join(*x) if is_List(x) else x), files) + existing = [f for f in files if os.path.exists(f)] + if existing: + print("Unexpected files exist: `%s'" % "', `".join(existing)) + self.fail_test(existing) + + def must_not_be_writable(self, *files): + """Ensures that the specified file(s) exist and are not writable. + An individual file can be specified as a list of directory names, + in which case the pathname will be constructed by concatenating + them. Exits FAILED if any of the files does not exist or is + writable. + """ + files = map((lambda x: os.path.join(*x) if is_List(x) else x), files) + existing, missing = separate_files(files) + writable = [x for x in existing if is_writable(x)] + if missing: + print("Missing files: `%s'" % "', `".join(missing)) + if writable: + print("Writable files: `%s'" % "', `".join(writable)) + self.fail_test(missing + writable) + + def _complete(self, actual_stdout, expected_stdout, + actual_stderr, expected_stderr, status, match): + """ + Post-processes running a subcommand, checking for failure + status and displaying output appropriately. + """ + if _failed(self, status): + expect = '' + if status != 0: + expect = " (expected %s)" % str(status) + print("%s returned %s%s" % (self.program, str(_status(self)), + expect)) + print(self.banner('STDOUT ')) + print(actual_stdout) + print(self.banner('STDERR ')) + print(actual_stderr) + self.fail_test() + if not expected_stdout is None and not match(actual_stdout, + expected_stdout): + self.diff(expected_stdout, actual_stdout, 'STDOUT ') + if actual_stderr: + print(self.banner('STDERR ')) + print(actual_stderr) + self.fail_test() + if not expected_stderr is None and not match(actual_stderr, + expected_stderr): + print(self.banner('STDOUT ')) + print(actual_stdout) + self.diff(expected_stderr, actual_stderr, 'STDERR ') + self.fail_test() + + def start(self, program = None, + interpreter = None, + arguments = None, + universal_newlines = None, + **kw): + """ + Starts a program or script for the test environment. + + This handles the "options" keyword argument and exceptions. + """ + options = kw.pop('options', None) + if options: + if arguments is None: + arguments = options + else: + arguments = options + " " + arguments + + try: + return TestCmd.start(self, program, interpreter, arguments, + universal_newlines, **kw) + except KeyboardInterrupt: + raise + except Exception as e: + print(self.banner('STDOUT ')) + try: + print(self.stdout()) + except IndexError: + pass + print(self.banner('STDERR ')) + try: + print(self.stderr()) + except IndexError: + pass + cmd_args = self.command_args(program, interpreter, arguments) + sys.stderr.write('Exception trying to execute: %s\n' % cmd_args) + raise e + + def finish(self, popen, stdout = None, stderr = '', status = 0, **kw): + """ + Finishes and waits for the process being run under control of + the specified popen argument. Additional arguments are similar + to those of the run() method: + + stdout The expected standard output from + the command. A value of None means + don't test standard output. + + stderr The expected error output from + the command. A value of None means + don't test error output. + + status The expected exit status from the + command. A value of None means don't + test exit status. + """ + TestCmd.finish(self, popen, **kw) + match = kw.get('match', self.match) + self._complete(self.stdout(), stdout, + self.stderr(), stderr, status, match) + + def run(self, options = None, arguments = None, + stdout = None, stderr = '', status = 0, **kw): + """Runs the program under test, checking that the test succeeded. + + The arguments are the same as the base TestCmd.run() method, + with the addition of: + + options Extra options that get appended to the beginning + of the arguments. + + stdout The expected standard output from + the command. A value of None means + don't test standard output. + + stderr The expected error output from + the command. A value of None means + don't test error output. + + status The expected exit status from the + command. A value of None means don't + test exit status. + + By default, this expects a successful exit (status = 0), does + not test standard output (stdout = None), and expects that error + output is empty (stderr = ""). + """ + if options: + if arguments is None: + arguments = options + else: + arguments = options + " " + arguments + kw['arguments'] = arguments + match = kw.pop('match', self.match) + TestCmd.run(self, **kw) + self._complete(self.stdout(), stdout, + self.stderr(), stderr, status, match) + + def skip_test(self, message="Skipping test.\n"): + """Skips a test. + + Proper test-skipping behavior is dependent on the external + TESTCOMMON_PASS_SKIPS environment variable. If set, we treat + the skip as a PASS (exit 0), and otherwise treat it as NO RESULT. + In either case, we print the specified message as an indication + that the substance of the test was skipped. + + (This was originally added to support development under Aegis. + Technically, skipping a test is a NO RESULT, but Aegis would + treat that as a test failure and prevent the change from going to + the next step. Since we ddn't want to force anyone using Aegis + to have to install absolutely every tool used by the tests, we + would actually report to Aegis that a skipped test has PASSED + so that the workflow isn't held up.) + """ + if message: + sys.stdout.write(message) + sys.stdout.flush() + pass_skips = os.environ.get('TESTCOMMON_PASS_SKIPS') + if pass_skips in [None, 0, '0']: + # skip=1 means skip this function when showing where this + # result came from. They only care about the line where the + # script called test.skip_test(), not the line number where + # we call test.no_result(). + self.no_result(skip=1) + else: + # We're under the development directory for this change, + # so this is an Aegis invocation; pass the test (exit 0). + self.pass_test() + +# Local Variables: +# tab-width:4 +# indent-tabs-mode:nil +# End: +# vim: set expandtab tabstop=4 shiftwidth=4: diff --git a/third_party/python/gyp/test/lib/TestGyp.py b/third_party/python/gyp/test/lib/TestGyp.py new file mode 100644 index 0000000000..2aa78684f5 --- /dev/null +++ b/third_party/python/gyp/test/lib/TestGyp.py @@ -0,0 +1,1260 @@ +# Copyright (c) 2012 Google Inc. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +""" +TestGyp.py: a testing framework for GYP integration tests. +""" +from __future__ import print_function + +import errno +import itertools +import os +import re +import shutil +import subprocess +import sys +import tempfile + +from contextlib import contextmanager + +from six.moves import collections_abc + +import TestCmd +import TestCommon +from TestCommon import __all__ + +__all__.extend([ + 'TestGyp', +]) + + +def remove_debug_line_numbers(contents): + """Function to remove the line numbers from the debug output + of gyp and thus reduce the extreme fragility of the stdout + comparison tests. + """ + lines = contents.splitlines() + # split each line on ":" + lines = [l.split(":", 3) for l in lines] + # join each line back together while ignoring the + # 3rd column which is the line number + lines = [len(l) > 3 and ":".join(l[3:]) or l for l in lines] + return "\n".join(lines) + + +def match_modulo_line_numbers(contents_a, contents_b): + """File contents matcher that ignores line numbers.""" + contents_a = remove_debug_line_numbers(contents_a) + contents_b = remove_debug_line_numbers(contents_b) + return TestCommon.match_exact(contents_a, contents_b) + + +@contextmanager +def LocalEnv(local_env): + """Context manager to provide a local OS environment.""" + old_env = os.environ.copy() + os.environ.update(local_env) + try: + yield + finally: + os.environ.clear() + os.environ.update(old_env) + + +class TestGypBase(TestCommon.TestCommon): + """ + Class for controlling end-to-end tests of gyp generators. + + Instantiating this class will create a temporary directory and + arrange for its destruction (via the TestCmd superclass) and + copy all of the non-gyptest files in the directory hierarchy of the + executing script. + + The default behavior is to test the 'gyp' or 'gyp.bat' file in the + current directory. An alternative may be specified explicitly on + instantiation, or by setting the TESTGYP_GYP environment variable. + + This class should be subclassed for each supported gyp generator + (format). Various abstract methods below define calling signatures + used by the test scripts to invoke builds on the generated build + configuration and to run executables generated by those builds. + """ + + formats = [] + build_tool = None + build_tool_list = [] + + _exe = TestCommon.exe_suffix + _obj = TestCommon.obj_suffix + shobj_ = TestCommon.shobj_prefix + _shobj = TestCommon.shobj_suffix + lib_ = TestCommon.lib_prefix + _lib = TestCommon.lib_suffix + dll_ = TestCommon.dll_prefix + _dll = TestCommon.dll_suffix + module_ = TestCommon.module_prefix + _module = TestCommon.module_suffix + + # Constants to represent different targets. + ALL = '__all__' + DEFAULT = '__default__' + + # Constants for different target types. + EXECUTABLE = '__executable__' + STATIC_LIB = '__static_lib__' + SHARED_LIB = '__shared_lib__' + LOADABLE_MODULE = '__loadable_module__' + + def __init__(self, gyp=None, *args, **kw): + self.origin_cwd = os.path.abspath(os.path.dirname(sys.argv[0])) + self.extra_args = sys.argv[1:] + + if not gyp: + gyp = os.environ.get('TESTGYP_GYP') + if not gyp: + if sys.platform == 'win32': + gyp = 'gyp.bat' + else: + gyp = 'gyp' + self.gyp = os.path.abspath(gyp) + self.no_parallel = False + + self.formats = [self.format] + + self.initialize_build_tool() + + kw.setdefault('match', TestCommon.match_exact) + + # Put test output in out/testworkarea by default. + # Use temporary names so there are no collisions. + workdir = os.path.join('out', kw.get('workdir', 'testworkarea')) + # Create work area if it doesn't already exist. + if not os.path.isdir(workdir): + os.makedirs(workdir) + + kw['workdir'] = tempfile.mktemp(prefix='testgyp.', dir=workdir) + + formats = kw.pop('formats', []) + + super(TestGypBase, self).__init__(*args, **kw) + + real_format = self.format.split('-')[-1] + excluded_formats = set([f for f in formats if f[0] == '!']) + included_formats = set(formats) - excluded_formats + if ('!'+real_format in excluded_formats or + included_formats and real_format not in included_formats): + msg = 'Invalid test for %r format; skipping test.\n' + self.skip_test(msg % self.format) + + self.copy_test_configuration(self.origin_cwd, self.workdir) + self.set_configuration(None) + + # Set $HOME so that gyp doesn't read the user's actual + # ~/.gyp/include.gypi file, which may contain variables + # and other settings that would change the output. + os.environ['HOME'] = self.workpath() + # Clear $GYP_DEFINES for the same reason. + if 'GYP_DEFINES' in os.environ: + del os.environ['GYP_DEFINES'] + # Override the user's language settings, which could + # otherwise make the output vary from what is expected. + os.environ['LC_ALL'] = 'C' + + def built_file_must_exist(self, name, type=None, **kw): + """ + Fails the test if the specified built file name does not exist. + """ + return self.must_exist(self.built_file_path(name, type, **kw)) + + def built_file_must_not_exist(self, name, type=None, **kw): + """ + Fails the test if the specified built file name exists. + """ + return self.must_not_exist(self.built_file_path(name, type, **kw)) + + def built_file_must_match(self, name, contents, **kw): + """ + Fails the test if the contents of the specified built file name + do not match the specified contents. + """ + return self.must_match(self.built_file_path(name, **kw), contents) + + def built_file_must_not_match(self, name, contents, **kw): + """ + Fails the test if the contents of the specified built file name + match the specified contents. + """ + return self.must_not_match(self.built_file_path(name, **kw), contents) + + def built_file_must_not_contain(self, name, contents, **kw): + """ + Fails the test if the specified built file name contains the specified + contents. + """ + return self.must_not_contain(self.built_file_path(name, **kw), contents) + + def copy_test_configuration(self, source_dir, dest_dir): + """ + Copies the test configuration from the specified source_dir + (the directory in which the test script lives) to the + specified dest_dir (a temporary working directory). + + This ignores all files and directories that begin with + the string 'gyptest', and all '.svn' subdirectories. + """ + for root, dirs, files in os.walk(source_dir): + if '.svn' in dirs: + dirs.remove('.svn') + dirs = [ d for d in dirs if not d.startswith('gyptest') ] + files = [ f for f in files if not f.startswith('gyptest') ] + for dirname in dirs: + source = os.path.join(root, dirname) + destination = source.replace(source_dir, dest_dir) + os.mkdir(destination) + if sys.platform != 'win32': + shutil.copystat(source, destination) + for filename in files: + source = os.path.join(root, filename) + destination = source.replace(source_dir, dest_dir) + shutil.copy2(source, destination) + + # The gyp tests are run with HOME pointing to |dest_dir| to provide an + # hermetic environment. Symlink login.keychain and the 'Provisioning + # Profiles' folder to allow codesign to access to the data required for + # signing binaries. + if sys.platform == 'darwin': + old_keychain = GetDefaultKeychainPath() + old_provisioning_profiles = os.path.join( + os.environ['HOME'], 'Library', 'MobileDevice', + 'Provisioning Profiles') + + new_keychain = os.path.join(dest_dir, 'Library', 'Keychains') + MakeDirs(new_keychain) + os.symlink(old_keychain, os.path.join(new_keychain, 'login.keychain')) + + if os.path.exists(old_provisioning_profiles): + new_provisioning_profiles = os.path.join( + dest_dir, 'Library', 'MobileDevice') + MakeDirs(new_provisioning_profiles) + os.symlink(old_provisioning_profiles, + os.path.join(new_provisioning_profiles, 'Provisioning Profiles')) + + def initialize_build_tool(self): + """ + Initializes the .build_tool attribute. + + Searches the .build_tool_list for an executable name on the user's + $PATH. The first tool on the list is used as-is if nothing is found + on the current $PATH. + """ + for build_tool in self.build_tool_list: + if not build_tool: + continue + if os.path.isabs(build_tool): + self.build_tool = build_tool + return + build_tool = self.where_is(build_tool) + if build_tool: + self.build_tool = build_tool + return + + if self.build_tool_list: + self.build_tool = self.build_tool_list[0] + + def relocate(self, source, destination): + """ + Renames (relocates) the specified source (usually a directory) + to the specified destination, creating the destination directory + first if necessary. + + Note: Don't use this as a generic "rename" operation. In the + future, "relocating" parts of a GYP tree may affect the state of + the test to modify the behavior of later method calls. + """ + destination_dir = os.path.dirname(destination) + if not os.path.exists(destination_dir): + self.subdir(destination_dir) + os.rename(source, destination) + + def report_not_up_to_date(self): + """ + Reports that a build is not up-to-date. + + This provides common reporting for formats that have complicated + conditions for checking whether a build is up-to-date. Formats + that expect exact output from the command (make) can + just set stdout= when they call the run_build() method. + """ + print("Build is not up-to-date:") + print(self.banner('STDOUT ')) + print(self.stdout()) + stderr = self.stderr() + if stderr: + print(self.banner('STDERR ')) + print(stderr) + + def run_gyp(self, gyp_file, *args, **kw): + """ + Runs gyp against the specified gyp_file with the specified args. + """ + + # When running gyp, and comparing its output we use a comparitor + # that ignores the line numbers that gyp logs in its debug output. + if kw.pop('ignore_line_numbers', False): + kw.setdefault('match', match_modulo_line_numbers) + + # TODO: --depth=. works around Chromium-specific tree climbing. + depth = kw.pop('depth', '.') + run_args = ['--depth='+depth] + run_args.extend(['--format='+f for f in self.formats]) + run_args.append(gyp_file) + if self.no_parallel: + run_args += ['--no-parallel'] + # TODO: if extra_args contains a '--build' flag + # we really want that to only apply to the last format (self.format). + run_args.extend(self.extra_args) + # Default xcode_ninja_target_pattern to ^.*$ to fix xcode-ninja tests + xcode_ninja_target_pattern = kw.pop('xcode_ninja_target_pattern', '.*') + if self is TestGypXcodeNinja: + run_args.extend( + ['-G', 'xcode_ninja_target_pattern=%s' % xcode_ninja_target_pattern]) + run_args.extend(args) + return self.run(program=self.gyp, arguments=run_args, **kw) + + def run(self, *args, **kw): + """ + Executes a program by calling the superclass .run() method. + + This exists to provide a common place to filter out keyword + arguments implemented in this layer, without having to update + the tool-specific subclasses or clutter the tests themselves + with platform-specific code. + """ + if 'SYMROOT' in kw: + del kw['SYMROOT'] + super(TestGypBase, self).run(*args, **kw) + + def set_configuration(self, configuration): + """ + Sets the configuration, to be used for invoking the build + tool and testing potential built output. + """ + self.configuration = configuration + + def configuration_dirname(self): + if self.configuration: + return self.configuration.split('|')[0] + else: + return 'Default' + + def configuration_buildname(self): + if self.configuration: + return self.configuration + else: + return 'Default' + + # + # Abstract methods to be defined by format-specific subclasses. + # + + def build(self, gyp_file, target=None, **kw): + """ + Runs a build of the specified target against the configuration + generated from the specified gyp_file. + + A 'target' argument of None or the special value TestGyp.DEFAULT + specifies the default argument for the underlying build tool. + A 'target' argument of TestGyp.ALL specifies the 'all' target + (if any) of the underlying build tool. + """ + raise NotImplementedError + + def built_file_path(self, name, type=None, **kw): + """ + Returns a path to the specified file name, of the specified type. + """ + raise NotImplementedError + + def built_file_basename(self, name, type=None, **kw): + """ + Returns the base name of the specified file name, of the specified type. + + A bare=True keyword argument specifies that prefixes and suffixes shouldn't + be applied. + """ + if not kw.get('bare'): + if type == self.EXECUTABLE: + name = name + self._exe + elif type == self.STATIC_LIB: + name = self.lib_ + name + self._lib + elif type == self.SHARED_LIB: + name = self.dll_ + name + self._dll + elif type == self.LOADABLE_MODULE: + name = self.module_ + name + self._module + return name + + def run_built_executable(self, name, *args, **kw): + """ + Runs an executable program built from a gyp-generated configuration. + + The specified name should be independent of any particular generator. + Subclasses should find the output executable in the appropriate + output build directory, tack on any necessary executable suffix, etc. + """ + raise NotImplementedError + + def up_to_date(self, gyp_file, target=None, **kw): + """ + Verifies that a build of the specified target is up to date. + + The subclass should implement this by calling build() + (or a reasonable equivalent), checking whatever conditions + will tell it the build was an "up to date" null build, and + failing if it isn't. + """ + raise NotImplementedError + + +class TestGypGypd(TestGypBase): + """ + Subclass for testing the GYP 'gypd' generator (spit out the + internal data structure as pretty-printed Python). + """ + format = 'gypd' + def __init__(self, gyp=None, *args, **kw): + super(TestGypGypd, self).__init__(*args, **kw) + # gypd implies the use of 'golden' files, so parallelizing conflicts as it + # causes ordering changes. + self.no_parallel = True + + +class TestGypCustom(TestGypBase): + """ + Subclass for testing the GYP with custom generator + """ + + def __init__(self, gyp=None, *args, **kw): + self.format = kw.pop("format") + super(TestGypCustom, self).__init__(*args, **kw) + + +class TestGypCMake(TestGypBase): + """ + Subclass for testing the GYP CMake generator, using cmake's ninja backend. + """ + format = 'cmake' + build_tool_list = ['cmake'] + ALL = 'all' + + def cmake_build(self, gyp_file, target=None, **kw): + arguments = kw.get('arguments', [])[:] + + self.build_tool_list = ['cmake'] + self.initialize_build_tool() + + chdir = os.path.join(kw.get('chdir', '.'), + 'out', + self.configuration_dirname()) + kw['chdir'] = chdir + + arguments.append('-G') + arguments.append('Ninja') + + kw['arguments'] = arguments + + stderr = kw.get('stderr', None) + if stderr: + kw['stderr'] = stderr.split('$$$')[0] + + self.run(program=self.build_tool, **kw) + + def ninja_build(self, gyp_file, target=None, **kw): + arguments = kw.get('arguments', [])[:] + + self.build_tool_list = ['ninja'] + self.initialize_build_tool() + + # Add a -C output/path to the command line. + arguments.append('-C') + arguments.append(os.path.join('out', self.configuration_dirname())) + + if target not in (None, self.DEFAULT): + arguments.append(target) + + kw['arguments'] = arguments + + stderr = kw.get('stderr', None) + if stderr: + stderrs = stderr.split('$$$') + kw['stderr'] = stderrs[1] if len(stderrs) > 1 else '' + + return self.run(program=self.build_tool, **kw) + + def build(self, gyp_file, target=None, status=0, **kw): + # Two tools must be run to build, cmake and the ninja. + # Allow cmake to succeed when the overall expectation is to fail. + if status is None: + kw['status'] = None + else: + if not isinstance(status, collections_abc.Iterable): status = (status,) + kw['status'] = list(itertools.chain((0,), status)) + self.cmake_build(gyp_file, target, **kw) + kw['status'] = status + self.ninja_build(gyp_file, target, **kw) + + def run_built_executable(self, name, *args, **kw): + # Enclosing the name in a list avoids prepending the original dir. + program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)] + if sys.platform == 'darwin': + configuration = self.configuration_dirname() + os.environ['DYLD_LIBRARY_PATH'] = os.path.join('out', configuration) + return self.run(program=program, *args, **kw) + + def built_file_path(self, name, type=None, **kw): + result = [] + chdir = kw.get('chdir') + if chdir: + result.append(chdir) + result.append('out') + result.append(self.configuration_dirname()) + if type == self.STATIC_LIB: + if sys.platform != 'darwin': + result.append('obj.target') + elif type == self.SHARED_LIB: + if sys.platform != 'darwin' and sys.platform != 'win32': + result.append('lib.target') + subdir = kw.get('subdir') + if subdir and type != self.SHARED_LIB: + result.append(subdir) + result.append(self.built_file_basename(name, type, **kw)) + return self.workpath(*result) + + def up_to_date(self, gyp_file, target=None, **kw): + result = self.ninja_build(gyp_file, target, **kw) + if not result: + stdout = self.stdout() + if 'ninja: no work to do' not in stdout: + self.report_not_up_to_date() + self.fail_test() + return result + + +class TestGypMake(TestGypBase): + """ + Subclass for testing the GYP Make generator. + """ + format = 'make' + build_tool_list = ['make'] + ALL = 'all' + def build(self, gyp_file, target=None, **kw): + """ + Runs a Make build using the Makefiles generated from the specified + gyp_file. + """ + arguments = kw.get('arguments', [])[:] + if self.configuration: + arguments.append('BUILDTYPE=' + self.configuration) + if target not in (None, self.DEFAULT): + arguments.append(target) + # Sub-directory builds provide per-gyp Makefiles (i.e. + # Makefile.gyp_filename), so use that if there is no Makefile. + chdir = kw.get('chdir', '') + if not os.path.exists(os.path.join(chdir, 'Makefile')): + print("NO Makefile in " + os.path.join(chdir, 'Makefile')) + arguments.insert(0, '-f') + arguments.insert(1, os.path.splitext(gyp_file)[0] + '.Makefile') + kw['arguments'] = arguments + return self.run(program=self.build_tool, **kw) + def up_to_date(self, gyp_file, target=None, **kw): + """ + Verifies that a build of the specified Make target is up to date. + """ + if target in (None, self.DEFAULT): + message_target = 'all' + else: + message_target = target + kw['stdout'] = "make: Nothing to be done for '%s'.\n" % message_target + return self.build(gyp_file, target, **kw) + def run_built_executable(self, name, *args, **kw): + """ + Runs an executable built by Make. + """ + configuration = self.configuration_dirname() + libdir = os.path.join('out', configuration, 'lib') + # TODO(piman): when everything is cross-compile safe, remove lib.target + if sys.platform == 'darwin': + # Mac puts target shared libraries right in the product directory. + configuration = self.configuration_dirname() + os.environ['DYLD_LIBRARY_PATH'] = ( + libdir + '.host:' + os.path.join('out', configuration)) + else: + os.environ['LD_LIBRARY_PATH'] = libdir + '.host:' + libdir + '.target' + # Enclosing the name in a list avoids prepending the original dir. + program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)] + return self.run(program=program, *args, **kw) + def built_file_path(self, name, type=None, **kw): + """ + Returns a path to the specified file name, of the specified type, + as built by Make. + + Built files are in the subdirectory 'out/{configuration}'. + The default is 'out/Default'. + + A chdir= keyword argument specifies the source directory + relative to which the output subdirectory can be found. + + "type" values of STATIC_LIB or SHARED_LIB append the necessary + prefixes and suffixes to a platform-independent library base name. + + A subdir= keyword argument specifies a library subdirectory within + the default 'obj.target'. + """ + result = [] + chdir = kw.get('chdir') + if chdir: + result.append(chdir) + configuration = self.configuration_dirname() + result.extend(['out', configuration]) + if type == self.STATIC_LIB and sys.platform != 'darwin': + result.append('obj.target') + elif type == self.SHARED_LIB and sys.platform != 'darwin': + result.append('lib.target') + subdir = kw.get('subdir') + if subdir and type != self.SHARED_LIB: + result.append(subdir) + result.append(self.built_file_basename(name, type, **kw)) + return self.workpath(*result) + + +def ConvertToCygpath(path): + """Convert to cygwin path if we are using cygwin.""" + if sys.platform == 'cygwin': + p = subprocess.Popen(['cygpath', path], stdout=subprocess.PIPE) + path = p.communicate()[0].strip() + return path + + +def MakeDirs(new_dir): + """A wrapper around os.makedirs() that emulates "mkdir -p".""" + try: + os.makedirs(new_dir) + except OSError as e: + if e.errno != errno.EEXIST: + raise + +def GetDefaultKeychainPath(): + """Get the keychain path, for used before updating HOME.""" + assert sys.platform == 'darwin' + # Format is: + # $ security default-keychain + # "/Some/Path/To/default.keychain" + path = subprocess.check_output(['security', 'default-keychain']).decode( + 'utf-8', 'ignore').strip() + return path[1:-1] + +def FindMSBuildInstallation(msvs_version = 'auto'): + """Returns path to MSBuild for msvs_version or latest available. + + Looks in the registry to find install location of MSBuild. + MSBuild before v4.0 will not build c++ projects, so only use newer versions. + """ + import TestWin + registry = TestWin.Registry() + + msvs_to_msbuild = { + '2013': r'12.0', + '2012': r'4.0', # Really v4.0.30319 which comes with .NET 4.5. + '2010': r'4.0'} + + msbuild_basekey = r'HKLM\SOFTWARE\Microsoft\MSBuild\ToolsVersions' + if not registry.KeyExists(msbuild_basekey): + print('Error: could not find MSBuild base registry entry') + return None + + msbuild_version = None + if msvs_version in msvs_to_msbuild: + msbuild_test_version = msvs_to_msbuild[msvs_version] + if registry.KeyExists(msbuild_basekey + '\\' + msbuild_test_version): + msbuild_version = msbuild_test_version + else: + print('Warning: Environment variable GYP_MSVS_VERSION specifies "%s" ' + 'but corresponding MSBuild "%s" was not found.' % + (msvs_version, msbuild_version)) + if not msbuild_version: + for msvs_version in sorted(msvs_to_msbuild, reverse=True): + msbuild_test_version = msvs_to_msbuild[msvs_version] + if registry.KeyExists(msbuild_basekey + '\\' + msbuild_test_version): + msbuild_version = msbuild_test_version + break + if not msbuild_version: + print('Error: could not find MSBuild registry entry') + return None + + msbuild_path = registry.GetValue(msbuild_basekey + '\\' + msbuild_version, + 'MSBuildToolsPath') + if not msbuild_path: + print('Error: could not get MSBuild registry entry value') + return None + + return os.path.join(msbuild_path, 'MSBuild.exe') + + +def FindVisualStudioInstallation(): + """Returns appropriate values for .build_tool and .uses_msbuild fields + of TestGypBase for Visual Studio. + + We use the value specified by GYP_MSVS_VERSION. If not specified, we + search %PATH% and %PATHEXT% for a devenv.{exe,bat,...} executable. + Failing that, we search for likely deployment paths. + """ + override_build_tool = os.environ.get('GYP_BUILD_TOOL') + if override_build_tool: + return override_build_tool, True, override_build_tool + + possible_roots = ['%s:\\Program Files%s' % (chr(drive), suffix) + for drive in range(ord('C'), ord('Z') + 1) + for suffix in ['', ' (x86)']] + possible_paths = { + '2017': r'Microsoft Visual Studio\2017', + '2015': r'Microsoft Visual Studio 14.0\Common7\IDE\devenv.com', + '2013': r'Microsoft Visual Studio 12.0\Common7\IDE\devenv.com', + '2012': r'Microsoft Visual Studio 11.0\Common7\IDE\devenv.com', + '2010': r'Microsoft Visual Studio 10.0\Common7\IDE\devenv.com', + '2008': r'Microsoft Visual Studio 9.0\Common7\IDE\devenv.com', + '2005': r'Microsoft Visual Studio 8\Common7\IDE\devenv.com'} + + possible_roots = [ConvertToCygpath(r) for r in possible_roots] + + msvs_version = 'auto' + for flag in (f for f in sys.argv if f.startswith('msvs_version=')): + msvs_version = flag.split('=')[-1] + msvs_version = os.environ.get('GYP_MSVS_VERSION', msvs_version) + + if msvs_version in ['2017', 'auto']: + msbuild_exes = [] + try: + path = possible_paths['2017'] + for r in possible_roots: + build_tool = os.path.join(r, path) + if os.path.exists(build_tool): + break; + else: + build_tool = None + if not build_tool: + args1 = ['reg', 'query', + 'HKLM\Software\Microsoft\VisualStudio\SxS\VS7', + '/v', '15.0', '/reg:32'] + build_tool = subprocess.check_output(args1).decode( + 'utf-8', 'ignore').strip().split(b'\r\n').pop().split(b' ').pop() + build_tool = build_tool.decode('utf-8') + if build_tool: + args2 = ['cmd.exe', '/d', '/c', + 'cd', '/d', build_tool, + '&', 'dir', '/b', '/s', 'msbuild.exe'] + msbuild_exes = subprocess.check_output(args2).strip().split(b'\r\n') + msbuild_exes = [m.decode('utf-8') for m in msbuild_exes] + if len(msbuild_exes): + msbuild_Path = os.path.join(build_tool, msbuild_exes[0]) + if os.path.exists(msbuild_Path): + os.environ['GYP_MSVS_VERSION'] = '2017' + os.environ['GYP_BUILD_TOOL'] = msbuild_Path + return msbuild_Path, True, msbuild_Path + except Exception as e: + pass + + if msvs_version in possible_paths: + # Check that the path to the specified GYP_MSVS_VERSION exists. + path = possible_paths[msvs_version] + for r in possible_roots: + build_tool = os.path.join(r, path) + if os.path.exists(build_tool): + uses_msbuild = msvs_version >= '2010' + msbuild_path = FindMSBuildInstallation(msvs_version) + return build_tool, uses_msbuild, msbuild_path + else: + print('Warning: Environment variable GYP_MSVS_VERSION specifies "%s" ' + 'but corresponding "%s" was not found.' % (msvs_version, path)) + # Neither GYP_MSVS_VERSION nor the path help us out. Iterate through + # the choices looking for a match. + for version in sorted(possible_paths, reverse=True): + path = possible_paths[version] + for r in possible_roots: + build_tool = os.path.join(r, path) + if os.path.exists(build_tool): + uses_msbuild = msvs_version >= '2010' + msbuild_path = FindMSBuildInstallation(msvs_version) + return build_tool, uses_msbuild, msbuild_path + print('Error: could not find devenv') + sys.exit(1) + +class TestGypOnMSToolchain(TestGypBase): + """ + Common subclass for testing generators that target the Microsoft Visual + Studio toolchain (cl, link, dumpbin, etc.) + """ + @staticmethod + def _ComputeVsvarsPath(devenv_path): + devenv_dir = os.path.split(devenv_path)[0] + + # Check for location of Community install (in VS2017, at least). + vcvars_path = os.path.join(devenv_path, '..', '..', '..', '..', 'VC', + 'Auxiliary', 'Build', 'vcvars32.bat') + if os.path.exists(vcvars_path): + return os.path.abspath(vcvars_path) + + vsvars_path = os.path.join(devenv_path, '..', '..', 'Tools', + 'vsvars32.bat') + return os.path.abspath(vsvars_path) + + def initialize_build_tool(self): + super(TestGypOnMSToolchain, self).initialize_build_tool() + if sys.platform in ('win32', 'cygwin'): + build_tools = FindVisualStudioInstallation() + self.devenv_path, self.uses_msbuild, self.msbuild_path = build_tools + self.vsvars_path = TestGypOnMSToolchain._ComputeVsvarsPath( + self.devenv_path) + + def run_dumpbin(self, *dumpbin_args): + """Run the dumpbin tool with the specified arguments, and capturing and + returning stdout.""" + assert sys.platform in ('win32', 'cygwin') + cmd = os.environ.get('COMSPEC', 'cmd.exe') + arguments = [cmd, '/c', self.vsvars_path, '&&', 'dumpbin'] + arguments.extend(dumpbin_args) + proc = subprocess.Popen(arguments, stdout=subprocess.PIPE) + output = proc.communicate()[0].decode('utf-8', 'ignore') + assert not proc.returncode + return output + +class TestGypNinja(TestGypOnMSToolchain): + """ + Subclass for testing the GYP Ninja generator. + """ + format = 'ninja' + build_tool_list = ['ninja'] + ALL = 'all' + DEFAULT = 'all' + + def run_gyp(self, gyp_file, *args, **kw): + TestGypBase.run_gyp(self, gyp_file, *args, **kw) + + def build(self, gyp_file, target=None, **kw): + arguments = kw.get('arguments', [])[:] + + # Add a -C output/path to the command line. + arguments.append('-C') + arguments.append(os.path.join('out', self.configuration_dirname())) + + if target is None: + target = 'all' + arguments.append(target) + + kw['arguments'] = arguments + return self.run(program=self.build_tool, **kw) + + def run_built_executable(self, name, *args, **kw): + # Enclosing the name in a list avoids prepending the original dir. + program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)] + if sys.platform == 'darwin': + configuration = self.configuration_dirname() + os.environ['DYLD_LIBRARY_PATH'] = os.path.join('out', configuration) + return self.run(program=program, *args, **kw) + + def built_file_path(self, name, type=None, **kw): + result = [] + chdir = kw.get('chdir') + if chdir: + result.append(chdir) + result.append('out') + result.append(self.configuration_dirname()) + if type == self.STATIC_LIB: + if sys.platform != 'darwin': + result.append('obj') + elif type == self.SHARED_LIB: + if sys.platform != 'darwin' and sys.platform != 'win32': + result.append('lib') + subdir = kw.get('subdir') + if subdir and type != self.SHARED_LIB: + result.append(subdir) + result.append(self.built_file_basename(name, type, **kw)) + return self.workpath(*result) + + def up_to_date(self, gyp_file, target=None, **kw): + result = self.build(gyp_file, target, **kw) + if not result: + stdout = self.stdout() + if 'ninja: no work to do' not in stdout: + self.report_not_up_to_date() + self.fail_test() + return result + + +class TestGypMSVS(TestGypOnMSToolchain): + """ + Subclass for testing the GYP Visual Studio generator. + """ + format = 'msvs' + + u = r'=== Build: 0 succeeded, 0 failed, (\d+) up-to-date, 0 skipped ===' + up_to_date_re = re.compile(u, re.M) + + # Initial None element will indicate to our .initialize_build_tool() + # method below that 'devenv' was not found on %PATH%. + # + # Note: we must use devenv.com to be able to capture build output. + # Directly executing devenv.exe only sends output to BuildLog.htm. + build_tool_list = [None, 'devenv.com'] + + def initialize_build_tool(self): + super(TestGypMSVS, self).initialize_build_tool() + self.build_tool = self.devenv_path + + def build(self, gyp_file, target=None, rebuild=False, clean=False, **kw): + """ + Runs a Visual Studio build using the configuration generated + from the specified gyp_file. + """ + if '15.0' in self.build_tool: + configuration = '/p:Configuration=' + ( + self.configuration or self.configuration_buildname()) + build = '/t' + if target not in (None, self.ALL, self.DEFAULT): + build += ':' + target + if clean: + build += ':Clean' + elif rebuild: + build += ':Rebuild' + elif ':' not in build: + build += ':Build' + arguments = kw.get('arguments', [])[:] + arguments.extend([gyp_file.replace('.gyp', '.sln'), + build, configuration]) + else: + configuration = self.configuration_buildname() + if clean: + build = '/Clean' + elif rebuild: + build = '/Rebuild' + else: + build = '/Build' + arguments = kw.get('arguments', [])[:] + arguments.extend([gyp_file.replace('.gyp', '.sln'), + build, configuration]) + # Note: the Visual Studio generator doesn't add an explicit 'all' + # target, so we just treat it the same as the default. + if target not in (None, self.ALL, self.DEFAULT): + arguments.extend(['/Project', target]) + if self.configuration: + arguments.extend(['/ProjectConfig', self.configuration]) + kw['arguments'] = arguments + return self.run(program=self.build_tool, **kw) + def up_to_date(self, gyp_file, target=None, **kw): + r""" + Verifies that a build of the specified Visual Studio target is up to date. + + Beware that VS2010 will behave strangely if you build under + C:\USERS\yourname\AppData\Local. It will cause needless work. The ouptut + will be "1 succeeded and 0 up to date". MSBuild tracing reveals that: + "Project 'C:\Users\...\AppData\Local\...vcxproj' not up to date because + 'C:\PROGRAM FILES (X86)\MICROSOFT VISUAL STUDIO 10.0\VC\BIN\1033\CLUI.DLL' + was modified at 02/21/2011 17:03:30, which is newer than '' which was + modified at 01/01/0001 00:00:00. + + The workaround is to specify a workdir when instantiating the test, e.g. + test = TestGyp.TestGyp(workdir='workarea') + """ + result = self.build(gyp_file, target, **kw) + if not result: + stdout = self.stdout() + + m = self.up_to_date_re.search(stdout) + up_to_date = m and int(m.group(1)) > 0 + if not up_to_date: + self.report_not_up_to_date() + self.fail_test() + return result + def run_built_executable(self, name, *args, **kw): + """ + Runs an executable built by Visual Studio. + """ + configuration = self.configuration_dirname() + # Enclosing the name in a list avoids prepending the original dir. + program = [self.built_file_path(name, type=self.EXECUTABLE, **kw)] + return self.run(program=program, *args, **kw) + def built_file_path(self, name, type=None, **kw): + """ + Returns a path to the specified file name, of the specified type, + as built by Visual Studio. + + Built files are in a subdirectory that matches the configuration + name. The default is 'Default'. + + A chdir= keyword argument specifies the source directory + relative to which the output subdirectory can be found. + + "type" values of STATIC_LIB or SHARED_LIB append the necessary + prefixes and suffixes to a platform-independent library base name. + """ + result = [] + chdir = kw.get('chdir') + if chdir: + result.append(chdir) + result.append(self.configuration_dirname()) + if type == self.STATIC_LIB: + result.append('lib') + result.append(self.built_file_basename(name, type, **kw)) + return self.workpath(*result) + + +class TestGypMSVSNinja(TestGypNinja): + """ + Subclass for testing the GYP Visual Studio Ninja generator. + """ + format = 'msvs-ninja' + + def initialize_build_tool(self): + super(TestGypMSVSNinja, self).initialize_build_tool() + # When using '--build', make sure ninja is first in the format list. + self.formats.insert(0, 'ninja') + + def build(self, gyp_file, target=None, rebuild=False, clean=False, **kw): + """ + Runs a Visual Studio build using the configuration generated + from the specified gyp_file. + """ + arguments = kw.get('arguments', [])[:] + if target in (None, self.ALL, self.DEFAULT): + # Note: the Visual Studio generator doesn't add an explicit 'all' target. + # This will build each project. This will work if projects are hermetic, + # but may fail if they are not (a project may run more than once). + # It would be nice to supply an all.metaproj for MSBuild. + arguments.extend([gyp_file.replace('.gyp', '.sln')]) + else: + # MSBuild documentation claims that one can specify a sln but then build a + # project target like 'msbuild a.sln /t:proj:target' but this format only + # supports 'Clean', 'Rebuild', and 'Publish' (with none meaning Default). + # This limitation is due to the .sln -> .sln.metaproj conversion. + # The ':' is not special, 'proj:target' is a target in the metaproj. + arguments.extend([target+'.vcxproj']) + + if clean: + build = 'Clean' + elif rebuild: + build = 'Rebuild' + else: + build = 'Build' + arguments.extend(['/target:'+build]) + configuration = self.configuration_buildname() + config = configuration.split('|') + arguments.extend(['/property:Configuration='+config[0]]) + if len(config) > 1: + arguments.extend(['/property:Platform='+config[1]]) + arguments.extend(['/property:BuildInParallel=false']) + arguments.extend(['/verbosity:minimal']) + + kw['arguments'] = arguments + return self.run(program=self.msbuild_path, **kw) + + +class TestGypXcode(TestGypBase): + """ + Subclass for testing the GYP Xcode generator. + """ + format = 'xcode' + build_tool_list = ['xcodebuild'] + + phase_script_execution = ("\n" + "PhaseScriptExecution /\\S+/Script-[0-9A-F]+\\.sh\n" + " cd /\\S+\n" + " /bin/sh -c /\\S+/Script-[0-9A-F]+\\.sh\n" + "(make: Nothing to be done for .all.\\.\n)?") + + strip_up_to_date_expressions = [ + # Various actions or rules can run even when the overall build target + # is up to date. Strip those phases' GYP-generated output. + re.compile(phase_script_execution, re.S), + + # The message from distcc_pump can trail the "BUILD SUCCEEDED" + # message, so strip that, too. + re.compile('__________Shutting down distcc-pump include server\n', re.S), + ] + + up_to_date_endings = ( + 'Checking Dependencies...\n** BUILD SUCCEEDED **\n', # Xcode 3.0/3.1 + 'Check dependencies\n** BUILD SUCCEEDED **\n\n', # Xcode 3.2 + 'Check dependencies\n\n\n** BUILD SUCCEEDED **\n\n', # Xcode 4.2 + 'Check dependencies\n\n** BUILD SUCCEEDED **\n\n', # Xcode 5.0 + ) + + def build(self, gyp_file, target=None, **kw): + """ + Runs an xcodebuild using the .xcodeproj generated from the specified + gyp_file. + """ + # Be sure we're working with a copy of 'arguments' since we modify it. + # The caller may not be expecting it to be modified. + arguments = kw.get('arguments', [])[:] + arguments.extend(['-project', gyp_file.replace('.gyp', '.xcodeproj')]) + if target == self.ALL: + arguments.append('-alltargets',) + elif target not in (None, self.DEFAULT): + arguments.extend(['-target', target]) + if self.configuration: + arguments.extend(['-configuration', self.configuration]) + symroot = kw.get('SYMROOT', '$SRCROOT/build') + if symroot: + arguments.append('SYMROOT='+symroot) + kw['arguments'] = arguments + + # Work around spurious stderr output from Xcode 4, http://crbug.com/181012 + match = kw.pop('match', self.match) + def match_filter_xcode(actual, expected): + if actual: + if not TestCmd.is_List(actual): + actual = actual.split('\n') + if not TestCmd.is_List(expected): + expected = expected.split('\n') + actual = [a for a in actual + if 'No recorder, buildTask: