1#!/usr/bin/env python3 2# Copyright (c) 2012 Google Inc. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""gyptest.py -- test runner for GYP tests.""" 7 8 9import argparse 10import os 11import platform 12import subprocess 13import sys 14import time 15 16 17def is_test_name(f): 18 return f.startswith("gyptest") and f.endswith(".py") 19 20 21def find_all_gyptest_files(directory): 22 result = [] 23 for root, dirs, files in os.walk(directory): 24 result.extend([os.path.join(root, f) for f in files if is_test_name(f)]) 25 result.sort() 26 return result 27 28 29def main(argv=None): 30 if argv is None: 31 argv = sys.argv 32 33 parser = argparse.ArgumentParser() 34 parser.add_argument("-a", "--all", action="store_true", help="run all tests") 35 parser.add_argument("-C", "--chdir", action="store", help="change to directory") 36 parser.add_argument( 37 "-f", 38 "--format", 39 action="store", 40 default="", 41 help="run tests with the specified formats", 42 ) 43 parser.add_argument( 44 "-G", 45 "--gyp_option", 46 action="append", 47 default=[], 48 help="Add -G options to the gyp command line", 49 ) 50 parser.add_argument( 51 "-l", "--list", action="store_true", help="list available tests and exit" 52 ) 53 parser.add_argument( 54 "-n", 55 "--no-exec", 56 action="store_true", 57 help="no execute, just print the command line", 58 ) 59 parser.add_argument( 60 "--path", action="append", default=[], help="additional $PATH directory" 61 ) 62 parser.add_argument( 63 "-q", 64 "--quiet", 65 action="store_true", 66 help="quiet, don't print anything unless there are failures", 67 ) 68 parser.add_argument( 69 "-v", 70 "--verbose", 71 action="store_true", 72 help="print configuration info and test results.", 73 ) 74 parser.add_argument("tests", nargs="*") 75 args = parser.parse_args(argv[1:]) 76 77 if args.chdir: 78 os.chdir(args.chdir) 79 80 if args.path: 81 extra_path = [os.path.abspath(p) for p in args.path] 82 extra_path = os.pathsep.join(extra_path) 83 os.environ["PATH"] = extra_path + os.pathsep + os.environ["PATH"] 84 85 if not args.tests: 86 if not args.all: 87 sys.stderr.write("Specify -a to get all tests.\n") 88 return 1 89 args.tests = ["test"] 90 91 tests = [] 92 for arg in args.tests: 93 if os.path.isdir(arg): 94 tests.extend(find_all_gyptest_files(os.path.normpath(arg))) 95 else: 96 if not is_test_name(os.path.basename(arg)): 97 print(arg, "is not a valid gyp test name.", file=sys.stderr) 98 sys.exit(1) 99 tests.append(arg) 100 101 if args.list: 102 for test in tests: 103 print(test) 104 sys.exit(0) 105 106 os.environ["PYTHONPATH"] = os.path.abspath("test/lib") 107 108 if args.verbose: 109 print_configuration_info() 110 111 if args.gyp_option and not args.quiet: 112 print("Extra Gyp options: %s\n" % args.gyp_option) 113 114 if args.format: 115 format_list = args.format.split(",") 116 else: 117 format_list = { 118 "aix5": ["make"], 119 "os400": ["make"], 120 "freebsd7": ["make"], 121 "freebsd8": ["make"], 122 "openbsd5": ["make"], 123 "cygwin": ["msvs"], 124 "win32": ["msvs", "ninja"], 125 "linux": ["make", "ninja"], 126 "linux2": ["make", "ninja"], 127 "linux3": ["make", "ninja"], 128 # TODO: Re-enable xcode-ninja. 129 # https://bugs.chromium.org/p/gyp/issues/detail?id=530 130 # 'darwin': ['make', 'ninja', 'xcode', 'xcode-ninja'], 131 "darwin": ["make", "ninja", "xcode"], 132 }[sys.platform] 133 134 gyp_options = [] 135 for option in args.gyp_option: 136 gyp_options += ["-G", option] 137 138 runner = Runner(format_list, tests, gyp_options, args.verbose) 139 runner.run() 140 141 if not args.quiet: 142 runner.print_results() 143 144 return 1 if runner.failures else 0 145 146 147def print_configuration_info(): 148 print("Test configuration:") 149 if sys.platform == "darwin": 150 sys.path.append(os.path.abspath("test/lib")) 151 import TestMac 152 153 print(f" Mac {platform.mac_ver()[0]} {platform.mac_ver()[2]}") 154 print(f" Xcode {TestMac.Xcode.Version()}") 155 elif sys.platform == "win32": 156 sys.path.append(os.path.abspath("pylib")) 157 import gyp.MSVSVersion 158 159 print(" Win %s %s\n" % platform.win32_ver()[0:2]) 160 print(" MSVS %s" % gyp.MSVSVersion.SelectVisualStudioVersion().Description()) 161 elif sys.platform in ("linux", "linux2"): 162 print(" Linux %s" % " ".join(platform.linux_distribution())) 163 print(f" Python {platform.python_version()}") 164 print(f" PYTHONPATH={os.environ['PYTHONPATH']}") 165 print() 166 167 168class Runner: 169 def __init__(self, formats, tests, gyp_options, verbose): 170 self.formats = formats 171 self.tests = tests 172 self.verbose = verbose 173 self.gyp_options = gyp_options 174 self.failures = [] 175 self.num_tests = len(formats) * len(tests) 176 num_digits = len(str(self.num_tests)) 177 self.fmt_str = "[%%%dd/%%%dd] (%%s) %%s" % (num_digits, num_digits) 178 self.isatty = sys.stdout.isatty() and not self.verbose 179 self.env = os.environ.copy() 180 self.hpos = 0 181 182 def run(self): 183 run_start = time.time() 184 185 i = 1 186 for fmt in self.formats: 187 for test in self.tests: 188 self.run_test(test, fmt, i) 189 i += 1 190 191 if self.isatty: 192 self.erase_current_line() 193 194 self.took = time.time() - run_start 195 196 def run_test(self, test, fmt, i): 197 if self.isatty: 198 self.erase_current_line() 199 200 msg = self.fmt_str % (i, self.num_tests, fmt, test) 201 self.print_(msg) 202 203 start = time.time() 204 cmd = [sys.executable, test] + self.gyp_options 205 self.env["TESTGYP_FORMAT"] = fmt 206 proc = subprocess.Popen( 207 cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=self.env 208 ) 209 proc.wait() 210 took = time.time() - start 211 212 stdout = proc.stdout.read().decode("utf8") 213 if proc.returncode == 2: 214 res = "skipped" 215 elif proc.returncode: 216 res = "failed" 217 self.failures.append(f"({test}) {fmt}") 218 else: 219 res = "passed" 220 res_msg = f" {res} {took:.3f}s" 221 self.print_(res_msg) 222 223 if stdout and not stdout.endswith(("PASSED\n", "NO RESULT\n")): 224 print() 225 print("\n".join(f" {line}" for line in stdout.splitlines())) 226 elif not self.isatty: 227 print() 228 229 def print_(self, msg): 230 print(msg, end="") 231 index = msg.rfind("\n") 232 if index == -1: 233 self.hpos += len(msg) 234 else: 235 self.hpos = len(msg) - index 236 sys.stdout.flush() 237 238 def erase_current_line(self): 239 print("\b" * self.hpos + " " * self.hpos + "\b" * self.hpos, end="") 240 sys.stdout.flush() 241 self.hpos = 0 242 243 def print_results(self): 244 num_failures = len(self.failures) 245 if num_failures: 246 print() 247 if num_failures == 1: 248 print("Failed the following test:") 249 else: 250 print("Failed the following %d tests:" % num_failures) 251 print("\t" + "\n\t".join(sorted(self.failures))) 252 print() 253 print( 254 "Ran %d tests in %.3fs, %d failed." 255 % (self.num_tests, self.took, num_failures) 256 ) 257 print() 258 259 260if __name__ == "__main__": 261 sys.exit(main()) 262