1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/js/src/tests/lib/manifest.py Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,378 @@ 1.4 +# Library for JSTest manifests. 1.5 +# 1.6 +# This includes classes for representing and parsing JS manifests. 1.7 + 1.8 +import os, os.path, re, sys 1.9 +from subprocess import Popen, PIPE 1.10 + 1.11 +from tests import TestCase 1.12 + 1.13 + 1.14 +def split_path_into_dirs(path): 1.15 + dirs = [path] 1.16 + 1.17 + while True: 1.18 + path, tail = os.path.split(path) 1.19 + if not tail: 1.20 + break 1.21 + dirs.append(path) 1.22 + return dirs 1.23 + 1.24 +class XULInfo: 1.25 + def __init__(self, abi, os, isdebug): 1.26 + self.abi = abi 1.27 + self.os = os 1.28 + self.isdebug = isdebug 1.29 + self.browserIsRemote = False 1.30 + 1.31 + def as_js(self): 1.32 + """Return JS that when executed sets up variables so that JS expression 1.33 + predicates on XUL build info evaluate properly.""" 1.34 + 1.35 + return ('var xulRuntime = { OS: "%s", XPCOMABI: "%s", shell: true };' + 1.36 + 'var isDebugBuild=%s; var Android=%s; var browserIsRemote=%s') % ( 1.37 + self.os, 1.38 + self.abi, 1.39 + str(self.isdebug).lower(), 1.40 + str(self.os == "Android").lower(), 1.41 + str(self.browserIsRemote).lower()) 1.42 + 1.43 + @classmethod 1.44 + def create(cls, jsdir): 1.45 + """Create a XULInfo based on the current platform's characteristics.""" 1.46 + 1.47 + # Our strategy is to find the autoconf.mk generated for the build and 1.48 + # read the values from there. 1.49 + 1.50 + # Find config/autoconf.mk. 1.51 + dirs = split_path_into_dirs(os.getcwd()) + split_path_into_dirs(jsdir) 1.52 + 1.53 + path = None 1.54 + for dir in dirs: 1.55 + _path = os.path.join(dir, 'config/autoconf.mk') 1.56 + if os.path.isfile(_path): 1.57 + path = _path 1.58 + break 1.59 + 1.60 + if path == None: 1.61 + print ("Can't find config/autoconf.mk on a directory containing the JS shell" 1.62 + " (searched from %s)") % jsdir 1.63 + sys.exit(1) 1.64 + 1.65 + # Read the values. 1.66 + val_re = re.compile(r'(TARGET_XPCOM_ABI|OS_TARGET|MOZ_DEBUG)\s*=\s*(.*)') 1.67 + kw = { 'isdebug': False } 1.68 + for line in open(path): 1.69 + m = val_re.match(line) 1.70 + if m: 1.71 + key, val = m.groups() 1.72 + val = val.rstrip() 1.73 + if key == 'TARGET_XPCOM_ABI': 1.74 + kw['abi'] = val 1.75 + if key == 'OS_TARGET': 1.76 + kw['os'] = val 1.77 + if key == 'MOZ_DEBUG': 1.78 + kw['isdebug'] = (val == '1') 1.79 + return cls(**kw) 1.80 + 1.81 +class XULInfoTester: 1.82 + def __init__(self, xulinfo, js_bin): 1.83 + self.js_prolog = xulinfo.as_js() 1.84 + self.js_bin = js_bin 1.85 + # Maps JS expr to evaluation result. 1.86 + self.cache = {} 1.87 + 1.88 + def test(self, cond): 1.89 + """Test a XUL predicate condition against this local info.""" 1.90 + ans = self.cache.get(cond, None) 1.91 + if ans is None: 1.92 + cmd = [ self.js_bin, '-e', self.js_prolog, '-e', 'print(!!(%s))'%cond ] 1.93 + p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) 1.94 + out, err = p.communicate() 1.95 + if out in ('true\n', 'true\r\n'): 1.96 + ans = True 1.97 + elif out in ('false\n', 'false\r\n'): 1.98 + ans = False 1.99 + else: 1.100 + raise Exception(("Failed to test XUL condition %r;" 1.101 + + " output was %r, stderr was %r") 1.102 + % (cond, out, err)) 1.103 + self.cache[cond] = ans 1.104 + return ans 1.105 + 1.106 +class NullXULInfoTester: 1.107 + """Can be used to parse manifests without a JS shell.""" 1.108 + def test(self, cond): 1.109 + return False 1.110 + 1.111 +def _parse_one(testcase, xul_tester): 1.112 + pos = 0 1.113 + parts = testcase.terms.split() 1.114 + while pos < len(parts): 1.115 + if parts[pos] == 'fails': 1.116 + testcase.expect = False 1.117 + pos += 1 1.118 + elif parts[pos] == 'skip': 1.119 + testcase.expect = testcase.enable = False 1.120 + pos += 1 1.121 + elif parts[pos] == 'random': 1.122 + testcase.random = True 1.123 + pos += 1 1.124 + elif parts[pos].startswith('fails-if'): 1.125 + cond = parts[pos][len('fails-if('):-1] 1.126 + if xul_tester.test(cond): 1.127 + testcase.expect = False 1.128 + pos += 1 1.129 + elif parts[pos].startswith('asserts-if'): 1.130 + # This directive means we may flunk some number of 1.131 + # NS_ASSERTIONs in the browser. For the shell, ignore it. 1.132 + pos += 1 1.133 + elif parts[pos].startswith('skip-if'): 1.134 + cond = parts[pos][len('skip-if('):-1] 1.135 + if xul_tester.test(cond): 1.136 + testcase.expect = testcase.enable = False 1.137 + pos += 1 1.138 + elif parts[pos].startswith('random-if'): 1.139 + cond = parts[pos][len('random-if('):-1] 1.140 + if xul_tester.test(cond): 1.141 + testcase.random = True 1.142 + pos += 1 1.143 + elif parts[pos].startswith('require-or'): 1.144 + cond = parts[pos][len('require-or('):-1] 1.145 + (preconditions, fallback_action) = re.split(",", cond) 1.146 + for precondition in re.split("&&", preconditions): 1.147 + if precondition == 'debugMode': 1.148 + testcase.options.append('-d') 1.149 + elif precondition == 'true': 1.150 + pass 1.151 + else: 1.152 + if fallback_action == "skip": 1.153 + testcase.expect = testcase.enable = False 1.154 + elif fallback_action == "fail": 1.155 + testcase.expect = False 1.156 + elif fallback_action == "random": 1.157 + testcase.random = True 1.158 + else: 1.159 + raise Exception(("Invalid precondition '%s' or fallback " + 1.160 + " action '%s'") % (precondition, fallback_action)) 1.161 + break 1.162 + pos += 1 1.163 + elif parts[pos] == 'slow': 1.164 + testcase.slow = True 1.165 + pos += 1 1.166 + elif parts[pos] == 'silentfail': 1.167 + # silentfails use tons of memory, and Darwin doesn't support ulimit. 1.168 + if xul_tester.test("xulRuntime.OS == 'Darwin'"): 1.169 + testcase.expect = testcase.enable = False 1.170 + pos += 1 1.171 + else: 1.172 + print 'warning: invalid manifest line element "%s"'%parts[pos] 1.173 + pos += 1 1.174 + 1.175 +def _build_manifest_script_entry(script_name, test): 1.176 + line = [] 1.177 + if test.terms: 1.178 + line.append(test.terms) 1.179 + line.append("script") 1.180 + line.append(script_name) 1.181 + if test.comment: 1.182 + line.append("#") 1.183 + line.append(test.comment) 1.184 + return ' '.join(line) 1.185 + 1.186 +def _map_prefixes_left(test_list): 1.187 + """ 1.188 + Splits tests into a dictionary keyed on the first component of the test 1.189 + path, aggregating tests with a common base path into a list. 1.190 + """ 1.191 + byprefix = {} 1.192 + for t in test_list: 1.193 + left, sep, remainder = t.path.partition(os.sep) 1.194 + if left not in byprefix: 1.195 + byprefix[left] = [] 1.196 + if remainder: 1.197 + t.path = remainder 1.198 + byprefix[left].append(t) 1.199 + return byprefix 1.200 + 1.201 +def _emit_manifest_at(location, relative, test_list, depth): 1.202 + """ 1.203 + location - str: absolute path where we want to write the manifest 1.204 + relative - str: relative path from topmost manifest directory to current 1.205 + test_list - [str]: list of all test paths and directorys 1.206 + depth - int: number of dirs we are below the topmost manifest dir 1.207 + """ 1.208 + manifests = _map_prefixes_left(test_list) 1.209 + 1.210 + filename = os.path.join(location, 'jstests.list') 1.211 + manifest = [] 1.212 + numTestFiles = 0 1.213 + for k, test_list in manifests.iteritems(): 1.214 + fullpath = os.path.join(location, k) 1.215 + if os.path.isdir(fullpath): 1.216 + manifest.append("include " + k + "/jstests.list") 1.217 + relpath = os.path.join(relative, k) 1.218 + _emit_manifest_at(fullpath, relpath, test_list, depth + 1) 1.219 + else: 1.220 + numTestFiles += 1 1.221 + assert len(test_list) == 1 1.222 + line = _build_manifest_script_entry(k, test_list[0]) 1.223 + manifest.append(line) 1.224 + 1.225 + # Always present our manifest in sorted order. 1.226 + manifest.sort() 1.227 + 1.228 + # If we have tests, we have to set the url-prefix so reftest can find them. 1.229 + if numTestFiles > 0: 1.230 + manifest = (["url-prefix %sjsreftest.html?test=%s/" % ('../' * depth, relative)] 1.231 + + manifest) 1.232 + 1.233 + fp = open(filename, 'w') 1.234 + try: 1.235 + fp.write('\n'.join(manifest) + '\n') 1.236 + finally: 1.237 + fp.close() 1.238 + 1.239 +def make_manifests(location, test_list): 1.240 + _emit_manifest_at(location, '', test_list, 0) 1.241 + 1.242 +def _find_all_js_files(base, location): 1.243 + for root, dirs, files in os.walk(location): 1.244 + root = root[len(base) + 1:] 1.245 + for fn in files: 1.246 + if fn.endswith('.js'): 1.247 + yield root, fn 1.248 + 1.249 +TEST_HEADER_PATTERN_INLINE = re.compile(r'//\s*\|(.*?)\|\s*(.*?)\s*(--\s*(.*))?$') 1.250 +TEST_HEADER_PATTERN_MULTI = re.compile(r'/\*\s*\|(.*?)\|\s*(.*?)\s*(--\s*(.*))?\*/') 1.251 + 1.252 +def _parse_test_header(fullpath, testcase, xul_tester): 1.253 + """ 1.254 + This looks a bit weird. The reason is that it needs to be efficient, since 1.255 + it has to be done on every test 1.256 + """ 1.257 + fp = open(fullpath, 'r') 1.258 + try: 1.259 + buf = fp.read(512) 1.260 + finally: 1.261 + fp.close() 1.262 + 1.263 + # Bail early if we do not start with a single comment. 1.264 + if not buf.startswith("//"): 1.265 + return 1.266 + 1.267 + # Extract the token. 1.268 + buf, _, _ = buf.partition('\n') 1.269 + matches = TEST_HEADER_PATTERN_INLINE.match(buf) 1.270 + 1.271 + if not matches: 1.272 + matches = TEST_HEADER_PATTERN_MULTI.match(buf) 1.273 + if not matches: 1.274 + return 1.275 + 1.276 + testcase.tag = matches.group(1) 1.277 + testcase.terms = matches.group(2) 1.278 + testcase.comment = matches.group(4) 1.279 + 1.280 + _parse_one(testcase, xul_tester) 1.281 + 1.282 +def _parse_external_manifest(filename, relpath): 1.283 + """ 1.284 + Reads an external manifest file for test suites whose individual test cases 1.285 + can't be decorated with reftest comments. 1.286 + filename - str: name of the manifest file 1.287 + relpath - str: relative path of the directory containing the manifest 1.288 + within the test suite 1.289 + """ 1.290 + entries = [] 1.291 + 1.292 + with open(filename, 'r') as fp: 1.293 + manifest_re = re.compile(r'^\s*(.*)\s+(include|script)\s+(\S+)$') 1.294 + for line in fp: 1.295 + line, _, comment = line.partition('#') 1.296 + line = line.strip() 1.297 + if not line: 1.298 + continue 1.299 + matches = manifest_re.match(line) 1.300 + if not matches: 1.301 + print('warning: unrecognized line in jstests.list: {0}'.format(line)) 1.302 + continue 1.303 + 1.304 + path = os.path.normpath(os.path.join(relpath, matches.group(3))) 1.305 + if matches.group(2) == 'include': 1.306 + # The manifest spec wants a reference to another manifest here, 1.307 + # but we need just the directory. We do need the trailing 1.308 + # separator so we don't accidentally match other paths of which 1.309 + # this one is a prefix. 1.310 + assert(path.endswith('jstests.list')) 1.311 + path = path[:-len('jstests.list')] 1.312 + 1.313 + entries.append({'path': path, 'terms': matches.group(1), 'comment': comment.strip()}) 1.314 + 1.315 + # if one directory name is a prefix of another, we want the shorter one first 1.316 + entries.sort(key=lambda x: x["path"]) 1.317 + return entries 1.318 + 1.319 +def _apply_external_manifests(filename, testcase, entries, xul_tester): 1.320 + for entry in entries: 1.321 + if filename.startswith(entry["path"]): 1.322 + # The reftest spec would require combining the terms (failure types) 1.323 + # that may already be defined in the test case with the terms 1.324 + # specified in entry; for example, a skip overrides a random, which 1.325 + # overrides a fails. Since we don't necessarily know yet in which 1.326 + # environment the test cases will be run, we'd also have to 1.327 + # consider skip-if, random-if, and fails-if with as-yet unresolved 1.328 + # conditions. 1.329 + # At this point, we use external manifests only for test cases 1.330 + # that can't have their own failure type comments, so we simply 1.331 + # use the terms for the most specific path. 1.332 + testcase.terms = entry["terms"] 1.333 + testcase.comment = entry["comment"] 1.334 + _parse_one(testcase, xul_tester) 1.335 + 1.336 +def load(location, xul_tester, reldir = ''): 1.337 + """ 1.338 + Locates all tests by walking the filesystem starting at |location|. 1.339 + Uses xul_tester to evaluate any test conditions in the test header. 1.340 + Failure type and comment for a test case can come from 1.341 + - an external manifest entry for the test case, 1.342 + - an external manifest entry for a containing directory, 1.343 + - most commonly: the header of the test case itself. 1.344 + """ 1.345 + # The list of tests that we are collecting. 1.346 + tests = [] 1.347 + 1.348 + # Any file whose basename matches something in this set is ignored. 1.349 + EXCLUDED = set(('browser.js', 'shell.js', 'jsref.js', 'template.js', 1.350 + 'user.js', 'sta.js', 1.351 + 'test262-browser.js', 'test262-shell.js', 1.352 + 'test402-browser.js', 'test402-shell.js', 1.353 + 'testBuiltInObject.js', 'testIntl.js', 1.354 + 'js-test-driver-begin.js', 'js-test-driver-end.js')) 1.355 + 1.356 + manifestFile = os.path.join(location, 'jstests.list') 1.357 + externalManifestEntries = _parse_external_manifest(manifestFile, '') 1.358 + 1.359 + for root, basename in _find_all_js_files(location, location): 1.360 + # Skip js files in the root test directory. 1.361 + if not root: 1.362 + continue 1.363 + 1.364 + # Skip files that we know are not tests. 1.365 + if basename in EXCLUDED: 1.366 + continue 1.367 + 1.368 + # Get the full path and relative location of the file. 1.369 + filename = os.path.join(root, basename) 1.370 + fullpath = os.path.join(location, filename) 1.371 + 1.372 + # Skip empty files. 1.373 + statbuf = os.stat(fullpath) 1.374 + if statbuf.st_size == 0: 1.375 + continue 1.376 + 1.377 + testcase = TestCase(os.path.join(reldir, filename)) 1.378 + _apply_external_manifests(filename, testcase, externalManifestEntries, xul_tester) 1.379 + _parse_test_header(fullpath, testcase, xul_tester) 1.380 + tests.append(testcase) 1.381 + return tests