| # Library for JSTest manifests. |
| # |
| # This includes classes for representing and parsing JS manifests. |
| |
| from __future__ import print_function |
| |
| import os, re, sys |
| from subprocess import Popen, PIPE |
| |
| from tests import RefTestCase |
| |
| |
| def split_path_into_dirs(path): |
| dirs = [path] |
| |
| while True: |
| path, tail = os.path.split(path) |
| if not tail: |
| break |
| dirs.append(path) |
| return dirs |
| |
| class XULInfo: |
| def __init__(self, abi, os, isdebug): |
| self.abi = abi |
| self.os = os |
| self.isdebug = isdebug |
| self.browserIsRemote = False |
| |
| def as_js(self): |
| """Return JS that when executed sets up variables so that JS expression |
| predicates on XUL build info evaluate properly.""" |
| |
| return ('var xulRuntime = {{ OS: "{}", XPCOMABI: "{}", shell: true }};' |
| 'var isDebugBuild={}; var Android={}; ' |
| 'var browserIsRemote={}'.format( |
| self.os, |
| self.abi, |
| str(self.isdebug).lower(), |
| str(self.os == "Android").lower(), |
| str(self.browserIsRemote).lower())) |
| |
| @classmethod |
| def create(cls, jsdir): |
| """Create a XULInfo based on the current platform's characteristics.""" |
| |
| # Our strategy is to find the autoconf.mk generated for the build and |
| # read the values from there. |
| |
| # Find config/autoconf.mk. |
| dirs = split_path_into_dirs(os.getcwd()) + split_path_into_dirs(jsdir) |
| |
| path = None |
| for dir in dirs: |
| _path = os.path.join(dir, 'config/autoconf.mk') |
| if os.path.isfile(_path): |
| path = _path |
| break |
| |
| if path == None: |
| print("Can't find config/autoconf.mk on a directory containing" |
| " the JS shell (searched from {})".format(jsdir)) |
| sys.exit(1) |
| |
| # Read the values. |
| val_re = re.compile(r'(TARGET_XPCOM_ABI|OS_TARGET|MOZ_DEBUG)\s*=\s*(.*)') |
| kw = {'isdebug': False} |
| for line in open(path): |
| m = val_re.match(line) |
| if m: |
| key, val = m.groups() |
| val = val.rstrip() |
| if key == 'TARGET_XPCOM_ABI': |
| kw['abi'] = val |
| if key == 'OS_TARGET': |
| kw['os'] = val |
| if key == 'MOZ_DEBUG': |
| kw['isdebug'] = (val == '1') |
| return cls(**kw) |
| |
| class XULInfoTester: |
| def __init__(self, xulinfo, js_bin): |
| self.js_prologue = xulinfo.as_js() |
| self.js_bin = js_bin |
| # Maps JS expr to evaluation result. |
| self.cache = {} |
| |
| def test(self, cond): |
| """Test a XUL predicate condition against this local info.""" |
| ans = self.cache.get(cond, None) |
| if ans is None: |
| cmd = [ |
| self.js_bin, |
| '-e', self.js_prologue, |
| '-e', 'print(!!({}))'.format(cond) |
| ] |
| p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) |
| out, err = p.communicate() |
| if out in ('true\n', 'true\r\n'): |
| ans = True |
| elif out in ('false\n', 'false\r\n'): |
| ans = False |
| else: |
| raise Exception("Failed to test XUL condition {!r};" |
| " output was {!r}, stderr was {!r}".format( |
| cond, out, err)) |
| self.cache[cond] = ans |
| return ans |
| |
| class NullXULInfoTester: |
| """Can be used to parse manifests without a JS shell.""" |
| def test(self, cond): |
| return False |
| |
| def _parse_one(testcase, xul_tester): |
| pos = 0 |
| parts = testcase.terms.split() |
| while pos < len(parts): |
| if parts[pos] == 'fails': |
| testcase.expect = False |
| pos += 1 |
| elif parts[pos] == 'skip': |
| testcase.expect = testcase.enable = False |
| pos += 1 |
| elif parts[pos] == 'random': |
| testcase.random = True |
| pos += 1 |
| elif parts[pos].startswith('fails-if'): |
| cond = parts[pos][len('fails-if('):-1] |
| if xul_tester.test(cond): |
| testcase.expect = False |
| pos += 1 |
| elif parts[pos].startswith('asserts-if'): |
| # This directive means we may flunk some number of |
| # NS_ASSERTIONs in the browser. For the shell, ignore it. |
| pos += 1 |
| elif parts[pos].startswith('skip-if'): |
| cond = parts[pos][len('skip-if('):-1] |
| if xul_tester.test(cond): |
| testcase.expect = testcase.enable = False |
| pos += 1 |
| elif parts[pos].startswith('random-if'): |
| cond = parts[pos][len('random-if('):-1] |
| if xul_tester.test(cond): |
| testcase.random = True |
| pos += 1 |
| elif parts[pos] == 'slow': |
| testcase.slow = True |
| pos += 1 |
| elif parts[pos] == 'silentfail': |
| # silentfails use tons of memory, and Darwin doesn't support ulimit. |
| if xul_tester.test("xulRuntime.OS == 'Darwin'"): |
| testcase.expect = testcase.enable = False |
| pos += 1 |
| else: |
| print('warning: invalid manifest line element "{}"'.format( |
| parts[pos])) |
| pos += 1 |
| |
| def _build_manifest_script_entry(script_name, test): |
| line = [] |
| if test.terms: |
| line.append(test.terms) |
| line.append("script") |
| line.append(script_name) |
| if test.comment: |
| line.append("#") |
| line.append(test.comment) |
| return ' '.join(line) |
| |
| def _map_prefixes_left(test_gen): |
| """ |
| Splits tests into a dictionary keyed on the first component of the test |
| path, aggregating tests with a common base path into a list. |
| """ |
| byprefix = {} |
| for t in test_gen: |
| left, sep, remainder = t.path.partition(os.sep) |
| if left not in byprefix: |
| byprefix[left] = [] |
| if remainder: |
| t.path = remainder |
| byprefix[left].append(t) |
| return byprefix |
| |
| def _emit_manifest_at(location, relative, test_gen, depth): |
| """ |
| location - str: absolute path where we want to write the manifest |
| relative - str: relative path from topmost manifest directory to current |
| test_gen - (str): generator of all test paths and directorys |
| depth - int: number of dirs we are below the topmost manifest dir |
| """ |
| manifests = _map_prefixes_left(test_gen) |
| |
| filename = os.path.join(location, 'jstests.list') |
| manifest = [] |
| numTestFiles = 0 |
| for k, test_list in manifests.iteritems(): |
| fullpath = os.path.join(location, k) |
| if os.path.isdir(fullpath): |
| manifest.append("include " + k + "/jstests.list") |
| relpath = os.path.join(relative, k) |
| _emit_manifest_at(fullpath, relpath, test_list, depth + 1) |
| else: |
| numTestFiles += 1 |
| if len(test_list) != 1: |
| import pdb; pdb.set_trace() |
| assert len(test_list) == 1 |
| line = _build_manifest_script_entry(k, test_list[0]) |
| manifest.append(line) |
| |
| # Always present our manifest in sorted order. |
| manifest.sort() |
| |
| # If we have tests, we have to set the url-prefix so reftest can find them. |
| if numTestFiles > 0: |
| manifest = ["url-prefix {}jsreftest.html?test={}/".format( |
| '../' * depth, relative)] + manifest |
| |
| fp = open(filename, 'w') |
| try: |
| fp.write('\n'.join(manifest) + '\n') |
| finally: |
| fp.close() |
| |
| def make_manifests(location, test_gen): |
| _emit_manifest_at(location, '', test_gen, 0) |
| |
| def _find_all_js_files(base, location): |
| for root, dirs, files in os.walk(location): |
| root = root[len(base) + 1:] |
| for fn in files: |
| if fn.endswith('.js'): |
| yield root, fn |
| |
| TEST_HEADER_PATTERN_INLINE = re.compile(r'//\s*\|(.*?)\|\s*(.*?)\s*(--\s*(.*))?$') |
| TEST_HEADER_PATTERN_MULTI = re.compile(r'/\*\s*\|(.*?)\|\s*(.*?)\s*(--\s*(.*))?\*/') |
| |
| def _parse_test_header(fullpath, testcase, xul_tester): |
| """ |
| This looks a bit weird. The reason is that it needs to be efficient, since |
| it has to be done on every test |
| """ |
| fp = open(fullpath, 'r') |
| try: |
| buf = fp.read(512) |
| finally: |
| fp.close() |
| |
| # Bail early if we do not start with a single comment. |
| if not buf.startswith("//"): |
| return |
| |
| # Extract the token. |
| buf, _, _ = buf.partition('\n') |
| matches = TEST_HEADER_PATTERN_INLINE.match(buf) |
| |
| if not matches: |
| matches = TEST_HEADER_PATTERN_MULTI.match(buf) |
| if not matches: |
| return |
| |
| testcase.tag = matches.group(1) |
| testcase.terms = matches.group(2) |
| testcase.comment = matches.group(4) |
| |
| _parse_one(testcase, xul_tester) |
| |
| def _parse_external_manifest(filename, relpath): |
| """ |
| Reads an external manifest file for test suites whose individual test cases |
| can't be decorated with reftest comments. |
| filename - str: name of the manifest file |
| relpath - str: relative path of the directory containing the manifest |
| within the test suite |
| """ |
| entries = [] |
| |
| with open(filename, 'r') as fp: |
| manifest_re = re.compile(r'^\s*(.*)\s+(include|script)\s+(\S+)$') |
| for line in fp: |
| line, _, comment = line.partition('#') |
| line = line.strip() |
| if not line: |
| continue |
| matches = manifest_re.match(line) |
| if not matches: |
| print('warning: unrecognized line in jstests.list:' |
| ' {0}'.format(line)) |
| continue |
| |
| path = os.path.normpath(os.path.join(relpath, matches.group(3))) |
| if matches.group(2) == 'include': |
| # The manifest spec wants a reference to another manifest here, |
| # but we need just the directory. We do need the trailing |
| # separator so we don't accidentally match other paths of which |
| # this one is a prefix. |
| assert(path.endswith('jstests.list')) |
| path = path[:-len('jstests.list')] |
| |
| entries.append({'path': path, 'terms': matches.group(1), |
| 'comment': comment.strip()}) |
| |
| # if one directory name is a prefix of another, we want the shorter one |
| # first |
| entries.sort(key=lambda x: x["path"]) |
| return entries |
| |
| def _apply_external_manifests(filename, testcase, entries, xul_tester): |
| for entry in entries: |
| if filename.startswith(entry["path"]): |
| # The reftest spec would require combining the terms (failure types) |
| # that may already be defined in the test case with the terms |
| # specified in entry; for example, a skip overrides a random, which |
| # overrides a fails. Since we don't necessarily know yet in which |
| # environment the test cases will be run, we'd also have to |
| # consider skip-if, random-if, and fails-if with as-yet unresolved |
| # conditions. |
| # At this point, we use external manifests only for test cases |
| # that can't have their own failure type comments, so we simply |
| # use the terms for the most specific path. |
| testcase.terms = entry["terms"] |
| testcase.comment = entry["comment"] |
| _parse_one(testcase, xul_tester) |
| |
| def _is_test_file(path_from_root, basename, filename, requested_paths, |
| excluded_paths): |
| # Any file whose basename matches something in this set is ignored. |
| EXCLUDED = set(('browser.js', 'shell.js', 'jsref.js', 'template.js', |
| 'user.js', 'sta.js', |
| 'test262-browser.js', 'test262-shell.js', |
| 'test402-browser.js', 'test402-shell.js', |
| 'testBuiltInObject.js', 'testIntl.js', |
| 'js-test-driver-begin.js', 'js-test-driver-end.js')) |
| |
| # Skip js files in the root test directory. |
| if not path_from_root: |
| return False |
| |
| # Skip files that we know are not tests. |
| if basename in EXCLUDED: |
| return False |
| |
| # If any tests are requested by name, skip tests that do not match. |
| if requested_paths \ |
| and not any(req in filename for req in requested_paths): |
| return False |
| |
| # Skip excluded tests. |
| if filename in excluded_paths: |
| return False |
| |
| return True |
| |
| |
| def count_tests(location, requested_paths, excluded_paths): |
| count = 0 |
| for root, basename in _find_all_js_files(location, location): |
| filename = os.path.join(root, basename) |
| if _is_test_file(root, basename, filename, requested_paths, excluded_paths): |
| count += 1 |
| return count |
| |
| |
| def load_reftests(location, requested_paths, excluded_paths, xul_tester, reldir=''): |
| """ |
| Locates all tests by walking the filesystem starting at |location|. |
| Uses xul_tester to evaluate any test conditions in the test header. |
| Failure type and comment for a test case can come from |
| - an external manifest entry for the test case, |
| - an external manifest entry for a containing directory, |
| - most commonly: the header of the test case itself. |
| """ |
| manifestFile = os.path.join(location, 'jstests.list') |
| externalManifestEntries = _parse_external_manifest(manifestFile, '') |
| |
| for root, basename in _find_all_js_files(location, location): |
| # Get the full path and relative location of the file. |
| filename = os.path.join(root, basename) |
| if not _is_test_file(root, basename, filename, requested_paths, excluded_paths): |
| continue |
| |
| # Skip empty files. |
| fullpath = os.path.join(location, filename) |
| statbuf = os.stat(fullpath) |
| |
| testcase = RefTestCase(os.path.join(reldir, filename)) |
| _apply_external_manifests(filename, testcase, externalManifestEntries, |
| xul_tester) |
| _parse_test_header(fullpath, testcase, xul_tester) |
| yield testcase |