Sat, 03 Jan 2015 20:18:00 +0100
Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.
michael@0 | 1 | # Library for JSTest manifests. |
michael@0 | 2 | # |
michael@0 | 3 | # This includes classes for representing and parsing JS manifests. |
michael@0 | 4 | |
michael@0 | 5 | import os, os.path, re, sys |
michael@0 | 6 | from subprocess import Popen, PIPE |
michael@0 | 7 | |
michael@0 | 8 | from tests import TestCase |
michael@0 | 9 | |
michael@0 | 10 | |
michael@0 | 11 | def split_path_into_dirs(path): |
michael@0 | 12 | dirs = [path] |
michael@0 | 13 | |
michael@0 | 14 | while True: |
michael@0 | 15 | path, tail = os.path.split(path) |
michael@0 | 16 | if not tail: |
michael@0 | 17 | break |
michael@0 | 18 | dirs.append(path) |
michael@0 | 19 | return dirs |
michael@0 | 20 | |
michael@0 | 21 | class XULInfo: |
michael@0 | 22 | def __init__(self, abi, os, isdebug): |
michael@0 | 23 | self.abi = abi |
michael@0 | 24 | self.os = os |
michael@0 | 25 | self.isdebug = isdebug |
michael@0 | 26 | self.browserIsRemote = False |
michael@0 | 27 | |
michael@0 | 28 | def as_js(self): |
michael@0 | 29 | """Return JS that when executed sets up variables so that JS expression |
michael@0 | 30 | predicates on XUL build info evaluate properly.""" |
michael@0 | 31 | |
michael@0 | 32 | return ('var xulRuntime = { OS: "%s", XPCOMABI: "%s", shell: true };' + |
michael@0 | 33 | 'var isDebugBuild=%s; var Android=%s; var browserIsRemote=%s') % ( |
michael@0 | 34 | self.os, |
michael@0 | 35 | self.abi, |
michael@0 | 36 | str(self.isdebug).lower(), |
michael@0 | 37 | str(self.os == "Android").lower(), |
michael@0 | 38 | str(self.browserIsRemote).lower()) |
michael@0 | 39 | |
michael@0 | 40 | @classmethod |
michael@0 | 41 | def create(cls, jsdir): |
michael@0 | 42 | """Create a XULInfo based on the current platform's characteristics.""" |
michael@0 | 43 | |
michael@0 | 44 | # Our strategy is to find the autoconf.mk generated for the build and |
michael@0 | 45 | # read the values from there. |
michael@0 | 46 | |
michael@0 | 47 | # Find config/autoconf.mk. |
michael@0 | 48 | dirs = split_path_into_dirs(os.getcwd()) + split_path_into_dirs(jsdir) |
michael@0 | 49 | |
michael@0 | 50 | path = None |
michael@0 | 51 | for dir in dirs: |
michael@0 | 52 | _path = os.path.join(dir, 'config/autoconf.mk') |
michael@0 | 53 | if os.path.isfile(_path): |
michael@0 | 54 | path = _path |
michael@0 | 55 | break |
michael@0 | 56 | |
michael@0 | 57 | if path == None: |
michael@0 | 58 | print ("Can't find config/autoconf.mk on a directory containing the JS shell" |
michael@0 | 59 | " (searched from %s)") % jsdir |
michael@0 | 60 | sys.exit(1) |
michael@0 | 61 | |
michael@0 | 62 | # Read the values. |
michael@0 | 63 | val_re = re.compile(r'(TARGET_XPCOM_ABI|OS_TARGET|MOZ_DEBUG)\s*=\s*(.*)') |
michael@0 | 64 | kw = { 'isdebug': False } |
michael@0 | 65 | for line in open(path): |
michael@0 | 66 | m = val_re.match(line) |
michael@0 | 67 | if m: |
michael@0 | 68 | key, val = m.groups() |
michael@0 | 69 | val = val.rstrip() |
michael@0 | 70 | if key == 'TARGET_XPCOM_ABI': |
michael@0 | 71 | kw['abi'] = val |
michael@0 | 72 | if key == 'OS_TARGET': |
michael@0 | 73 | kw['os'] = val |
michael@0 | 74 | if key == 'MOZ_DEBUG': |
michael@0 | 75 | kw['isdebug'] = (val == '1') |
michael@0 | 76 | return cls(**kw) |
michael@0 | 77 | |
michael@0 | 78 | class XULInfoTester: |
michael@0 | 79 | def __init__(self, xulinfo, js_bin): |
michael@0 | 80 | self.js_prolog = xulinfo.as_js() |
michael@0 | 81 | self.js_bin = js_bin |
michael@0 | 82 | # Maps JS expr to evaluation result. |
michael@0 | 83 | self.cache = {} |
michael@0 | 84 | |
michael@0 | 85 | def test(self, cond): |
michael@0 | 86 | """Test a XUL predicate condition against this local info.""" |
michael@0 | 87 | ans = self.cache.get(cond, None) |
michael@0 | 88 | if ans is None: |
michael@0 | 89 | cmd = [ self.js_bin, '-e', self.js_prolog, '-e', 'print(!!(%s))'%cond ] |
michael@0 | 90 | p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) |
michael@0 | 91 | out, err = p.communicate() |
michael@0 | 92 | if out in ('true\n', 'true\r\n'): |
michael@0 | 93 | ans = True |
michael@0 | 94 | elif out in ('false\n', 'false\r\n'): |
michael@0 | 95 | ans = False |
michael@0 | 96 | else: |
michael@0 | 97 | raise Exception(("Failed to test XUL condition %r;" |
michael@0 | 98 | + " output was %r, stderr was %r") |
michael@0 | 99 | % (cond, out, err)) |
michael@0 | 100 | self.cache[cond] = ans |
michael@0 | 101 | return ans |
michael@0 | 102 | |
michael@0 | 103 | class NullXULInfoTester: |
michael@0 | 104 | """Can be used to parse manifests without a JS shell.""" |
michael@0 | 105 | def test(self, cond): |
michael@0 | 106 | return False |
michael@0 | 107 | |
michael@0 | 108 | def _parse_one(testcase, xul_tester): |
michael@0 | 109 | pos = 0 |
michael@0 | 110 | parts = testcase.terms.split() |
michael@0 | 111 | while pos < len(parts): |
michael@0 | 112 | if parts[pos] == 'fails': |
michael@0 | 113 | testcase.expect = False |
michael@0 | 114 | pos += 1 |
michael@0 | 115 | elif parts[pos] == 'skip': |
michael@0 | 116 | testcase.expect = testcase.enable = False |
michael@0 | 117 | pos += 1 |
michael@0 | 118 | elif parts[pos] == 'random': |
michael@0 | 119 | testcase.random = True |
michael@0 | 120 | pos += 1 |
michael@0 | 121 | elif parts[pos].startswith('fails-if'): |
michael@0 | 122 | cond = parts[pos][len('fails-if('):-1] |
michael@0 | 123 | if xul_tester.test(cond): |
michael@0 | 124 | testcase.expect = False |
michael@0 | 125 | pos += 1 |
michael@0 | 126 | elif parts[pos].startswith('asserts-if'): |
michael@0 | 127 | # This directive means we may flunk some number of |
michael@0 | 128 | # NS_ASSERTIONs in the browser. For the shell, ignore it. |
michael@0 | 129 | pos += 1 |
michael@0 | 130 | elif parts[pos].startswith('skip-if'): |
michael@0 | 131 | cond = parts[pos][len('skip-if('):-1] |
michael@0 | 132 | if xul_tester.test(cond): |
michael@0 | 133 | testcase.expect = testcase.enable = False |
michael@0 | 134 | pos += 1 |
michael@0 | 135 | elif parts[pos].startswith('random-if'): |
michael@0 | 136 | cond = parts[pos][len('random-if('):-1] |
michael@0 | 137 | if xul_tester.test(cond): |
michael@0 | 138 | testcase.random = True |
michael@0 | 139 | pos += 1 |
michael@0 | 140 | elif parts[pos].startswith('require-or'): |
michael@0 | 141 | cond = parts[pos][len('require-or('):-1] |
michael@0 | 142 | (preconditions, fallback_action) = re.split(",", cond) |
michael@0 | 143 | for precondition in re.split("&&", preconditions): |
michael@0 | 144 | if precondition == 'debugMode': |
michael@0 | 145 | testcase.options.append('-d') |
michael@0 | 146 | elif precondition == 'true': |
michael@0 | 147 | pass |
michael@0 | 148 | else: |
michael@0 | 149 | if fallback_action == "skip": |
michael@0 | 150 | testcase.expect = testcase.enable = False |
michael@0 | 151 | elif fallback_action == "fail": |
michael@0 | 152 | testcase.expect = False |
michael@0 | 153 | elif fallback_action == "random": |
michael@0 | 154 | testcase.random = True |
michael@0 | 155 | else: |
michael@0 | 156 | raise Exception(("Invalid precondition '%s' or fallback " + |
michael@0 | 157 | " action '%s'") % (precondition, fallback_action)) |
michael@0 | 158 | break |
michael@0 | 159 | pos += 1 |
michael@0 | 160 | elif parts[pos] == 'slow': |
michael@0 | 161 | testcase.slow = True |
michael@0 | 162 | pos += 1 |
michael@0 | 163 | elif parts[pos] == 'silentfail': |
michael@0 | 164 | # silentfails use tons of memory, and Darwin doesn't support ulimit. |
michael@0 | 165 | if xul_tester.test("xulRuntime.OS == 'Darwin'"): |
michael@0 | 166 | testcase.expect = testcase.enable = False |
michael@0 | 167 | pos += 1 |
michael@0 | 168 | else: |
michael@0 | 169 | print 'warning: invalid manifest line element "%s"'%parts[pos] |
michael@0 | 170 | pos += 1 |
michael@0 | 171 | |
michael@0 | 172 | def _build_manifest_script_entry(script_name, test): |
michael@0 | 173 | line = [] |
michael@0 | 174 | if test.terms: |
michael@0 | 175 | line.append(test.terms) |
michael@0 | 176 | line.append("script") |
michael@0 | 177 | line.append(script_name) |
michael@0 | 178 | if test.comment: |
michael@0 | 179 | line.append("#") |
michael@0 | 180 | line.append(test.comment) |
michael@0 | 181 | return ' '.join(line) |
michael@0 | 182 | |
michael@0 | 183 | def _map_prefixes_left(test_list): |
michael@0 | 184 | """ |
michael@0 | 185 | Splits tests into a dictionary keyed on the first component of the test |
michael@0 | 186 | path, aggregating tests with a common base path into a list. |
michael@0 | 187 | """ |
michael@0 | 188 | byprefix = {} |
michael@0 | 189 | for t in test_list: |
michael@0 | 190 | left, sep, remainder = t.path.partition(os.sep) |
michael@0 | 191 | if left not in byprefix: |
michael@0 | 192 | byprefix[left] = [] |
michael@0 | 193 | if remainder: |
michael@0 | 194 | t.path = remainder |
michael@0 | 195 | byprefix[left].append(t) |
michael@0 | 196 | return byprefix |
michael@0 | 197 | |
michael@0 | 198 | def _emit_manifest_at(location, relative, test_list, depth): |
michael@0 | 199 | """ |
michael@0 | 200 | location - str: absolute path where we want to write the manifest |
michael@0 | 201 | relative - str: relative path from topmost manifest directory to current |
michael@0 | 202 | test_list - [str]: list of all test paths and directorys |
michael@0 | 203 | depth - int: number of dirs we are below the topmost manifest dir |
michael@0 | 204 | """ |
michael@0 | 205 | manifests = _map_prefixes_left(test_list) |
michael@0 | 206 | |
michael@0 | 207 | filename = os.path.join(location, 'jstests.list') |
michael@0 | 208 | manifest = [] |
michael@0 | 209 | numTestFiles = 0 |
michael@0 | 210 | for k, test_list in manifests.iteritems(): |
michael@0 | 211 | fullpath = os.path.join(location, k) |
michael@0 | 212 | if os.path.isdir(fullpath): |
michael@0 | 213 | manifest.append("include " + k + "/jstests.list") |
michael@0 | 214 | relpath = os.path.join(relative, k) |
michael@0 | 215 | _emit_manifest_at(fullpath, relpath, test_list, depth + 1) |
michael@0 | 216 | else: |
michael@0 | 217 | numTestFiles += 1 |
michael@0 | 218 | assert len(test_list) == 1 |
michael@0 | 219 | line = _build_manifest_script_entry(k, test_list[0]) |
michael@0 | 220 | manifest.append(line) |
michael@0 | 221 | |
michael@0 | 222 | # Always present our manifest in sorted order. |
michael@0 | 223 | manifest.sort() |
michael@0 | 224 | |
michael@0 | 225 | # If we have tests, we have to set the url-prefix so reftest can find them. |
michael@0 | 226 | if numTestFiles > 0: |
michael@0 | 227 | manifest = (["url-prefix %sjsreftest.html?test=%s/" % ('../' * depth, relative)] |
michael@0 | 228 | + manifest) |
michael@0 | 229 | |
michael@0 | 230 | fp = open(filename, 'w') |
michael@0 | 231 | try: |
michael@0 | 232 | fp.write('\n'.join(manifest) + '\n') |
michael@0 | 233 | finally: |
michael@0 | 234 | fp.close() |
michael@0 | 235 | |
michael@0 | 236 | def make_manifests(location, test_list): |
michael@0 | 237 | _emit_manifest_at(location, '', test_list, 0) |
michael@0 | 238 | |
michael@0 | 239 | def _find_all_js_files(base, location): |
michael@0 | 240 | for root, dirs, files in os.walk(location): |
michael@0 | 241 | root = root[len(base) + 1:] |
michael@0 | 242 | for fn in files: |
michael@0 | 243 | if fn.endswith('.js'): |
michael@0 | 244 | yield root, fn |
michael@0 | 245 | |
michael@0 | 246 | TEST_HEADER_PATTERN_INLINE = re.compile(r'//\s*\|(.*?)\|\s*(.*?)\s*(--\s*(.*))?$') |
michael@0 | 247 | TEST_HEADER_PATTERN_MULTI = re.compile(r'/\*\s*\|(.*?)\|\s*(.*?)\s*(--\s*(.*))?\*/') |
michael@0 | 248 | |
michael@0 | 249 | def _parse_test_header(fullpath, testcase, xul_tester): |
michael@0 | 250 | """ |
michael@0 | 251 | This looks a bit weird. The reason is that it needs to be efficient, since |
michael@0 | 252 | it has to be done on every test |
michael@0 | 253 | """ |
michael@0 | 254 | fp = open(fullpath, 'r') |
michael@0 | 255 | try: |
michael@0 | 256 | buf = fp.read(512) |
michael@0 | 257 | finally: |
michael@0 | 258 | fp.close() |
michael@0 | 259 | |
michael@0 | 260 | # Bail early if we do not start with a single comment. |
michael@0 | 261 | if not buf.startswith("//"): |
michael@0 | 262 | return |
michael@0 | 263 | |
michael@0 | 264 | # Extract the token. |
michael@0 | 265 | buf, _, _ = buf.partition('\n') |
michael@0 | 266 | matches = TEST_HEADER_PATTERN_INLINE.match(buf) |
michael@0 | 267 | |
michael@0 | 268 | if not matches: |
michael@0 | 269 | matches = TEST_HEADER_PATTERN_MULTI.match(buf) |
michael@0 | 270 | if not matches: |
michael@0 | 271 | return |
michael@0 | 272 | |
michael@0 | 273 | testcase.tag = matches.group(1) |
michael@0 | 274 | testcase.terms = matches.group(2) |
michael@0 | 275 | testcase.comment = matches.group(4) |
michael@0 | 276 | |
michael@0 | 277 | _parse_one(testcase, xul_tester) |
michael@0 | 278 | |
michael@0 | 279 | def _parse_external_manifest(filename, relpath): |
michael@0 | 280 | """ |
michael@0 | 281 | Reads an external manifest file for test suites whose individual test cases |
michael@0 | 282 | can't be decorated with reftest comments. |
michael@0 | 283 | filename - str: name of the manifest file |
michael@0 | 284 | relpath - str: relative path of the directory containing the manifest |
michael@0 | 285 | within the test suite |
michael@0 | 286 | """ |
michael@0 | 287 | entries = [] |
michael@0 | 288 | |
michael@0 | 289 | with open(filename, 'r') as fp: |
michael@0 | 290 | manifest_re = re.compile(r'^\s*(.*)\s+(include|script)\s+(\S+)$') |
michael@0 | 291 | for line in fp: |
michael@0 | 292 | line, _, comment = line.partition('#') |
michael@0 | 293 | line = line.strip() |
michael@0 | 294 | if not line: |
michael@0 | 295 | continue |
michael@0 | 296 | matches = manifest_re.match(line) |
michael@0 | 297 | if not matches: |
michael@0 | 298 | print('warning: unrecognized line in jstests.list: {0}'.format(line)) |
michael@0 | 299 | continue |
michael@0 | 300 | |
michael@0 | 301 | path = os.path.normpath(os.path.join(relpath, matches.group(3))) |
michael@0 | 302 | if matches.group(2) == 'include': |
michael@0 | 303 | # The manifest spec wants a reference to another manifest here, |
michael@0 | 304 | # but we need just the directory. We do need the trailing |
michael@0 | 305 | # separator so we don't accidentally match other paths of which |
michael@0 | 306 | # this one is a prefix. |
michael@0 | 307 | assert(path.endswith('jstests.list')) |
michael@0 | 308 | path = path[:-len('jstests.list')] |
michael@0 | 309 | |
michael@0 | 310 | entries.append({'path': path, 'terms': matches.group(1), 'comment': comment.strip()}) |
michael@0 | 311 | |
michael@0 | 312 | # if one directory name is a prefix of another, we want the shorter one first |
michael@0 | 313 | entries.sort(key=lambda x: x["path"]) |
michael@0 | 314 | return entries |
michael@0 | 315 | |
michael@0 | 316 | def _apply_external_manifests(filename, testcase, entries, xul_tester): |
michael@0 | 317 | for entry in entries: |
michael@0 | 318 | if filename.startswith(entry["path"]): |
michael@0 | 319 | # The reftest spec would require combining the terms (failure types) |
michael@0 | 320 | # that may already be defined in the test case with the terms |
michael@0 | 321 | # specified in entry; for example, a skip overrides a random, which |
michael@0 | 322 | # overrides a fails. Since we don't necessarily know yet in which |
michael@0 | 323 | # environment the test cases will be run, we'd also have to |
michael@0 | 324 | # consider skip-if, random-if, and fails-if with as-yet unresolved |
michael@0 | 325 | # conditions. |
michael@0 | 326 | # At this point, we use external manifests only for test cases |
michael@0 | 327 | # that can't have their own failure type comments, so we simply |
michael@0 | 328 | # use the terms for the most specific path. |
michael@0 | 329 | testcase.terms = entry["terms"] |
michael@0 | 330 | testcase.comment = entry["comment"] |
michael@0 | 331 | _parse_one(testcase, xul_tester) |
michael@0 | 332 | |
michael@0 | 333 | def load(location, xul_tester, reldir = ''): |
michael@0 | 334 | """ |
michael@0 | 335 | Locates all tests by walking the filesystem starting at |location|. |
michael@0 | 336 | Uses xul_tester to evaluate any test conditions in the test header. |
michael@0 | 337 | Failure type and comment for a test case can come from |
michael@0 | 338 | - an external manifest entry for the test case, |
michael@0 | 339 | - an external manifest entry for a containing directory, |
michael@0 | 340 | - most commonly: the header of the test case itself. |
michael@0 | 341 | """ |
michael@0 | 342 | # The list of tests that we are collecting. |
michael@0 | 343 | tests = [] |
michael@0 | 344 | |
michael@0 | 345 | # Any file whose basename matches something in this set is ignored. |
michael@0 | 346 | EXCLUDED = set(('browser.js', 'shell.js', 'jsref.js', 'template.js', |
michael@0 | 347 | 'user.js', 'sta.js', |
michael@0 | 348 | 'test262-browser.js', 'test262-shell.js', |
michael@0 | 349 | 'test402-browser.js', 'test402-shell.js', |
michael@0 | 350 | 'testBuiltInObject.js', 'testIntl.js', |
michael@0 | 351 | 'js-test-driver-begin.js', 'js-test-driver-end.js')) |
michael@0 | 352 | |
michael@0 | 353 | manifestFile = os.path.join(location, 'jstests.list') |
michael@0 | 354 | externalManifestEntries = _parse_external_manifest(manifestFile, '') |
michael@0 | 355 | |
michael@0 | 356 | for root, basename in _find_all_js_files(location, location): |
michael@0 | 357 | # Skip js files in the root test directory. |
michael@0 | 358 | if not root: |
michael@0 | 359 | continue |
michael@0 | 360 | |
michael@0 | 361 | # Skip files that we know are not tests. |
michael@0 | 362 | if basename in EXCLUDED: |
michael@0 | 363 | continue |
michael@0 | 364 | |
michael@0 | 365 | # Get the full path and relative location of the file. |
michael@0 | 366 | filename = os.path.join(root, basename) |
michael@0 | 367 | fullpath = os.path.join(location, filename) |
michael@0 | 368 | |
michael@0 | 369 | # Skip empty files. |
michael@0 | 370 | statbuf = os.stat(fullpath) |
michael@0 | 371 | if statbuf.st_size == 0: |
michael@0 | 372 | continue |
michael@0 | 373 | |
michael@0 | 374 | testcase = TestCase(os.path.join(reldir, filename)) |
michael@0 | 375 | _apply_external_manifests(filename, testcase, externalManifestEntries, xul_tester) |
michael@0 | 376 | _parse_test_header(fullpath, testcase, xul_tester) |
michael@0 | 377 | tests.append(testcase) |
michael@0 | 378 | return tests |