summaryrefslogtreecommitdiffstats
path: root/js/src/tests/lib/manifest.py
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /js/src/tests/lib/manifest.py
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/src/tests/lib/manifest.py')
-rw-r--r--js/src/tests/lib/manifest.py641
1 files changed, 641 insertions, 0 deletions
diff --git a/js/src/tests/lib/manifest.py b/js/src/tests/lib/manifest.py
new file mode 100644
index 0000000000..834bcab088
--- /dev/null
+++ b/js/src/tests/lib/manifest.py
@@ -0,0 +1,641 @@
+# Library for JSTest manifests.
+#
+# This includes classes for representing and parsing JS manifests.
+
+import io
+import os
+import posixpath
+import re
+import sys
+from subprocess import PIPE, Popen
+
+import six
+
+from .remote import init_device
+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 release_or_beta = getBuildConfiguration('release_or_beta');"
+ "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 = posixpath.join(dir, "config", "autoconf.mk")
+ if os.path.isfile(_path):
+ path = _path
+ break
+
+ if path is 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 io.open(path, encoding="utf-8"):
+ 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, options, js_args):
+ self.js_prologue = xulinfo.as_js()
+ self.js_bin = options.js_shell
+ self.js_args = js_args
+ # options here are the command line options
+ self.options = options
+ # Maps JS expr to evaluation result.
+ self.cache = {}
+
+ if not self.options.remote:
+ return
+ self.device = init_device(options)
+ self.js_bin = posixpath.join(options.remote_test_root, "bin", "js")
+
+ def test(self, cond, options=[]):
+ if self.options.remote:
+ return self._test_remote(cond, options=options)
+ return self._test_local(cond, options=options)
+
+ def _test_remote(self, cond, options=[]):
+ from mozdevice import ADBDevice, ADBProcessError
+
+ ans = self.cache.get(cond, None)
+ if ans is not None:
+ return ans
+
+ env = {
+ "LD_LIBRARY_PATH": posixpath.join(self.options.remote_test_root, "bin"),
+ }
+
+ cmd = (
+ [self.js_bin]
+ + self.js_args
+ + options
+ + [
+ # run in safe configuration, since it is hard to debug
+ # crashes when running code here. In particular, msan will
+ # error out if the jit is active.
+ "--no-baseline",
+ "--no-blinterp",
+ "-e",
+ self.js_prologue,
+ "-e",
+ "print(!!({}))".format(cond),
+ ]
+ )
+ cmd = ADBDevice._escape_command_line(cmd)
+ try:
+ # Allow ADBError or ADBTimeoutError to terminate the test run,
+ # but handle ADBProcessError in order to support the use of
+ # non-zero exit codes in the JavaScript shell tests.
+ out = self.device.shell_output(
+ cmd, env=env, cwd=self.options.remote_test_root, timeout=None
+ )
+ err = ""
+ except ADBProcessError as e:
+ out = ""
+ err = str(e.adb_process.stdout)
+
+ if out == "true":
+ ans = True
+ elif out == "false":
+ 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
+
+ def _test_local(self, cond, options=[]):
+ """Test a XUL predicate condition against this local info."""
+ ans = self.cache.get(cond, None)
+ if ans is None:
+ cmd = (
+ [self.js_bin]
+ + self.js_args
+ + options
+ + [
+ # run in safe configuration, since it is hard to debug
+ # crashes when running code here. In particular, msan will
+ # error out if the jit is active.
+ "--no-baseline",
+ "--no-blinterp",
+ "-e",
+ self.js_prologue,
+ "-e",
+ "print(!!({}))".format(cond),
+ ]
+ )
+ p = Popen(
+ cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE, universal_newlines=True
+ )
+ 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, options=[]):
+ return False
+
+
+def _parse_one(testcase, terms, xul_tester):
+ pos = 0
+ parts = 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("shell-option("):
+ # This directive adds an extra option to pass to the shell.
+ option = parts[pos][len("shell-option(") : -1]
+ testcase.options.append(option)
+ pos += 1
+ elif parts[pos].startswith("fails-if"):
+ cond = parts[pos][len("fails-if(") : -1]
+ if xul_tester.test(cond, testcase.options):
+ 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.options):
+ testcase.expect = testcase.enable = False
+ pos += 1
+ elif parts[pos].startswith("ignore-flag"):
+ flag = parts[pos][len("ignore-flag(") : -1]
+ testcase.ignoredflags.append(flag)
+ pos += 1
+ elif parts[pos].startswith("random-if"):
+ cond = parts[pos][len("random-if(") : -1]
+ if xul_tester.test(cond, testcase.options):
+ testcase.random = True
+ pos += 1
+ elif parts[pos] == "slow":
+ testcase.slow = True
+ pos += 1
+ elif parts[pos].startswith("slow-if"):
+ cond = parts[pos][len("slow-if(") : -1]
+ if xul_tester.test(cond, testcase.options):
+ 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.options):
+ testcase.expect = testcase.enable = False
+ pos += 1
+ elif parts[pos].startswith("error:"):
+ # This directive allows to specify an error type.
+ (_, _, errortype) = parts[pos].partition(":")
+ testcase.error = errortype
+ pos += 1
+ elif parts[pos] == "module":
+ # This directive marks the test as module code.
+ testcase.is_module = True
+ pos += 1
+ elif parts[pos] == "test262-raw":
+ testcase.is_test262_raw = True
+ pos += 1
+ elif parts[pos] == "async":
+ # This directive marks the test as async.
+ testcase.is_async = True
+ pos += 1
+ else:
+ print('warning: invalid manifest line element "{}"'.format(parts[pos]))
+ pos += 1
+
+
+def _build_manifest_script_entry(script_name, test):
+ line = []
+ properties = []
+ if test.terms:
+ # Remove jsreftest internal terms.
+ terms = " ".join(
+ [
+ term
+ for term in test.terms.split()
+ if not (
+ term == "module"
+ or term == "async"
+ or term.startswith("error:")
+ or term.startswith("ignore-flag(")
+ or term.startswith("shell-option(")
+ or term == "test262-raw"
+ )
+ ]
+ )
+ if terms:
+ line.append(terms)
+ if test.error:
+ properties.append("error=" + test.error)
+ if test.is_module:
+ properties.append("module")
+ if test.is_async:
+ properties.append("async")
+ if test.is_test262_raw:
+ properties.append("test262_raw")
+ line.append("script")
+ script = script_name
+ if properties:
+ script = ";".join([script] + properties)
+ line.append(script)
+ 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.items():
+ 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
+ assert len(test_list) == 1, test_list
+ 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 = io.open(filename, "w", encoding="utf-8", newline="\n")
+ 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(location):
+ for root, dirs, files in os.walk(location):
+ root = root[len(location) + 1 :]
+ for fn in files:
+ if fn.endswith(".js"):
+ yield root, fn
+
+
+# The pattern for test header lines.
+TEST_HEADER_PATTERN = r"""
+# Ignore any space before the tag.
+\s*
+
+# The reftest tag is enclosed in pipes.
+\|(?P<tag>.*?)\|
+
+# Ignore any space before the options.
+\s*
+
+# Accept some options.
+(?P<options>.*?)
+
+# Ignore space before the comments.
+\s*
+
+# Accept an optional comment starting with "--".
+(?:
+ # Unless "--" is directly preceded by "(".
+ (?<!\()
+ --
+
+ # Ignore more space.
+ \s*
+
+ # The actual comment.
+ (?P<comment>.*)
+)?
+"""
+
+
+TEST_HEADER_PATTERN_INLINE = re.compile(
+ r"""
+# Start a single line comment
+//
+"""
+ + TEST_HEADER_PATTERN
+ + r"""
+# Match the end of line.
+$
+""",
+ re.VERBOSE,
+)
+TEST_HEADER_PATTERN_MULTI = re.compile(
+ r"""
+# Start a multi line comment
+/\*
+"""
+ + TEST_HEADER_PATTERN
+ + r"""
+# Match the end of comment.
+\*/
+""",
+ re.VERBOSE,
+)
+
+
+def _append_terms_and_comment(testcase, terms, comment):
+ if testcase.terms is None:
+ testcase.terms = terms
+ else:
+ testcase.terms += " " + terms
+
+ if testcase.comment is None:
+ testcase.comment = comment
+ elif comment:
+ testcase.comment += "; " + comment
+
+
+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
+ """
+ if six.PY3:
+ fp = open(fullpath, encoding="utf-8")
+ else:
+ fp = open(fullpath)
+ 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("tag")
+ _append_terms_and_comment(
+ testcase, matches.group("options"), matches.group("comment")
+ )
+ _parse_one(testcase, matches.group("options"), 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
+ """
+ if not os.path.exists(filename):
+ return []
+
+ entries = []
+
+ with io.open(filename, "r", encoding="utf-8") as fp:
+ manifest_re = re.compile(
+ r"^\s*(?P<terms>.*)\s+(?P<type>include|script)\s+(?P<path>\S+)$"
+ )
+ include_re = re.compile(r"^\s*include\s+(?P<path>\S+)$")
+ for line in fp:
+ line, _, comment = line.partition("#")
+ line = line.strip()
+ if not line:
+ continue
+ matches = manifest_re.match(line)
+ if not matches:
+ matches = include_re.match(line)
+ if not matches:
+ print(
+ "warning: unrecognized line in jstests.list:"
+ " {0}".format(line)
+ )
+ continue
+
+ include_file = matches.group("path")
+ include_filename = os.path.join(os.path.dirname(filename), include_file)
+ include_relpath = os.path.join(relpath, os.path.dirname(include_file))
+ include_entries = _parse_external_manifest(
+ include_filename, include_relpath
+ )
+ entries.extend(include_entries)
+ continue
+
+ path = os.path.normpath(os.path.join(relpath, matches.group("path")))
+ if matches.group("type") == "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("terms"),
+ "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.
+ _append_terms_and_comment(testcase, entry["terms"], entry["comment"])
+ _parse_one(testcase, entry["terms"], xul_tester)
+
+
+def _is_test_file(path_from_root, basename, filename, path_options):
+ # Any file whose basename matches something in this set is ignored.
+ EXCLUDED = set(
+ (
+ "browser.js",
+ "shell.js",
+ "template.js",
+ "user.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 not path_options.should_run(filename):
+ return False
+
+ return True
+
+
+def count_tests(location, path_options):
+ count = 0
+ for root, basename in _find_all_js_files(location):
+ filename = os.path.join(root, basename)
+ if _is_test_file(root, basename, filename, path_options):
+ count += 1
+ return count
+
+
+def load_reftests(location, path_options, xul_tester):
+ """
+ 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):
+ # Get the full path and relative location of the file.
+ filename = os.path.join(root, basename)
+ if not _is_test_file(root, basename, filename, path_options):
+ continue
+
+ # Skip empty files.
+ fullpath = os.path.join(location, filename)
+
+ testcase = RefTestCase(location, filename)
+ _apply_external_manifests(
+ filename, testcase, externalManifestEntries, xul_tester
+ )
+ _parse_test_header(fullpath, testcase, xul_tester)
+ yield testcase