diff options
Diffstat (limited to 'testing/web-platform/tests/tools/manifest/sourcefile.py')
-rw-r--r-- | testing/web-platform/tests/tools/manifest/sourcefile.py | 366 |
1 files changed, 366 insertions, 0 deletions
diff --git a/testing/web-platform/tests/tools/manifest/sourcefile.py b/testing/web-platform/tests/tools/manifest/sourcefile.py new file mode 100644 index 000000000..44a462707 --- /dev/null +++ b/testing/web-platform/tests/tools/manifest/sourcefile.py @@ -0,0 +1,366 @@ +import imp +import os +import re +from six.moves.urllib.parse import urljoin +from fnmatch import fnmatch +try: + from xml.etree import cElementTree as ElementTree +except ImportError: + from xml.etree import ElementTree + +import html5lib + +from . import vcs +from .item import Stub, ManualTest, WebdriverSpecTest, RefTest, TestharnessTest +from .utils import rel_path_to_url, is_blacklisted, ContextManagerBytesIO, cached_property + +wd_pattern = "*.py" +meta_re = re.compile("//\s*<meta>\s*(\w*)=(.*)$") + +def replace_end(s, old, new): + """ + Given a string `s` that ends with `old`, replace that occurrence of `old` + with `new`. + """ + assert s.endswith(old) + return s[:-len(old)] + new + + +class SourceFile(object): + parsers = {"html":lambda x:html5lib.parse(x, treebuilder="etree"), + "xhtml":ElementTree.parse, + "svg":ElementTree.parse} + + def __init__(self, tests_root, rel_path, url_base, use_committed=False, + contents=None): + """Object representing a file in a source tree. + + :param tests_root: Path to the root of the source tree + :param rel_path: File path relative to tests_root + :param url_base: Base URL used when converting file paths to urls + :param use_committed: Work with the last committed version of the file + rather than the on-disk version. + :param contents: Byte array of the contents of the file or ``None``. + """ + + assert not (use_committed and contents is not None) + + self.tests_root = tests_root + self.rel_path = rel_path + self.url_base = url_base + self.use_committed = use_committed + self.contents = contents + + self.url = rel_path_to_url(rel_path, url_base) + self.path = os.path.join(tests_root, rel_path) + + self.dir_path, self.filename = os.path.split(self.path) + self.name, self.ext = os.path.splitext(self.filename) + + self.type_flag = None + if "-" in self.name: + self.type_flag = self.name.rsplit("-", 1)[1].split(".")[0] + + self.meta_flags = self.name.split(".")[1:] + + def __getstate__(self): + # Remove computed properties if we pickle this class + rv = self.__dict__.copy() + + if "__cached_properties__" in rv: + cached_properties = rv["__cached_properties__"] + for key in rv.keys(): + if key in cached_properties: + del rv[key] + del rv["__cached_properties__"] + return rv + + def name_prefix(self, prefix): + """Check if the filename starts with a given prefix + + :param prefix: The prefix to check""" + return self.name.startswith(prefix) + + def is_dir(self): + """Return whether this file represents a directory.""" + if self.contents is not None: + return False + + return os.path.isdir(self.rel_path) + + def open(self): + """ + Return either + * the contents specified in the constructor, if any; + * the contents of the file when last committed, if use_committed is true; or + * a File object opened for reading the file contents. + """ + + if self.contents is not None: + file_obj = ContextManagerBytesIO(self.contents) + elif self.use_committed: + git = vcs.get_git_func(os.path.dirname(__file__)) + blob = git("show", "HEAD:%s" % self.rel_path) + file_obj = ContextManagerBytesIO(blob) + else: + file_obj = open(self.path, 'rb') + return file_obj + + @property + def name_is_non_test(self): + """Check if the file name matches the conditions for the file to + be a non-test file""" + return (self.is_dir() or + self.name_prefix("MANIFEST") or + self.filename.startswith(".") or + is_blacklisted(self.url)) + + @property + def name_is_stub(self): + """Check if the file name matches the conditions for the file to + be a stub file""" + return self.name_prefix("stub-") + + @property + def name_is_manual(self): + """Check if the file name matches the conditions for the file to + be a manual test file""" + return self.type_flag == "manual" + + @property + def name_is_multi_global(self): + """Check if the file name matches the conditions for the file to + be a multi-global js test file""" + return "any" in self.meta_flags and self.ext == ".js" + + @property + def name_is_worker(self): + """Check if the file name matches the conditions for the file to + be a worker js test file""" + return "worker" in self.meta_flags and self.ext == ".js" + + @property + def name_is_webdriver(self): + """Check if the file name matches the conditions for the file to + be a webdriver spec test file""" + # wdspec tests are in subdirectories of /webdriver excluding __init__.py + # files. + rel_dir_tree = self.rel_path.split(os.path.sep) + return (rel_dir_tree[0] == "webdriver" and + len(rel_dir_tree) > 1 and + self.filename != "__init__.py" and + fnmatch(self.filename, wd_pattern)) + + @property + def name_is_reference(self): + """Check if the file name matches the conditions for the file to + be a reference file (not a reftest)""" + return self.type_flag in ("ref", "notref") + + @property + def markup_type(self): + """Return the type of markup contained in a file, based on its extension, + or None if it doesn't contain markup""" + ext = self.ext + + if not ext: + return None + if ext[0] == ".": + ext = ext[1:] + if ext in ["html", "htm"]: + return "html" + if ext in ["xhtml", "xht", "xml"]: + return "xhtml" + if ext == "svg": + return "svg" + return None + + @cached_property + def root(self): + """Return an ElementTree Element for the root node of the file if it contains + markup, or None if it does not""" + if not self.markup_type: + return None + + parser = self.parsers[self.markup_type] + + with self.open() as f: + try: + tree = parser(f) + except Exception: + return None + + if hasattr(tree, "getroot"): + root = tree.getroot() + else: + root = tree + + return root + + @cached_property + def timeout_nodes(self): + """List of ElementTree Elements corresponding to nodes in a test that + specify timeouts""" + return self.root.findall(".//{http://www.w3.org/1999/xhtml}meta[@name='timeout']") + + @cached_property + def timeout(self): + """The timeout of a test or reference file. "long" if the file has an extended timeout + or None otherwise""" + if self.name_is_worker: + with self.open() as f: + for line in f: + m = meta_re.match(line) + if m and m.groups()[0] == "timeout": + if m.groups()[1].lower() == "long": + return "long" + return + + if not self.root: + return + + if self.timeout_nodes: + timeout_str = self.timeout_nodes[0].attrib.get("content", None) + if timeout_str and timeout_str.lower() == "long": + return timeout_str + + @cached_property + def viewport_nodes(self): + """List of ElementTree Elements corresponding to nodes in a test that + specify viewport sizes""" + return self.root.findall(".//{http://www.w3.org/1999/xhtml}meta[@name='viewport-size']") + + @cached_property + def viewport_size(self): + """The viewport size of a test or reference file""" + if not self.root: + return None + + if not self.viewport_nodes: + return None + + return self.viewport_nodes[0].attrib.get("content", None) + + @cached_property + def dpi_nodes(self): + """List of ElementTree Elements corresponding to nodes in a test that + specify device pixel ratios""" + return self.root.findall(".//{http://www.w3.org/1999/xhtml}meta[@name='device-pixel-ratio']") + + @cached_property + def dpi(self): + """The device pixel ratio of a test or reference file""" + if not self.root: + return None + + if not self.dpi_nodes: + return None + + return self.dpi_nodes[0].attrib.get("content", None) + + @cached_property + def testharness_nodes(self): + """List of ElementTree Elements corresponding to nodes representing a + testharness.js script""" + return self.root.findall(".//{http://www.w3.org/1999/xhtml}script[@src='/resources/testharness.js']") + + @cached_property + def content_is_testharness(self): + """Boolean indicating whether the file content represents a + testharness.js test""" + if not self.root: + return None + return bool(self.testharness_nodes) + + @cached_property + def variant_nodes(self): + """List of ElementTree Elements corresponding to nodes representing a + test variant""" + return self.root.findall(".//{http://www.w3.org/1999/xhtml}meta[@name='variant']") + + @cached_property + def test_variants(self): + rv = [] + for element in self.variant_nodes: + if "content" in element.attrib: + variant = element.attrib["content"] + assert variant == "" or variant[0] in ["#", "?"] + rv.append(variant) + + if not rv: + rv = [""] + + return rv + + @cached_property + def reftest_nodes(self): + """List of ElementTree Elements corresponding to nodes representing a + to a reftest <link>""" + if not self.root: + return [] + + match_links = self.root.findall(".//{http://www.w3.org/1999/xhtml}link[@rel='match']") + mismatch_links = self.root.findall(".//{http://www.w3.org/1999/xhtml}link[@rel='mismatch']") + return match_links + mismatch_links + + @cached_property + def references(self): + """List of (ref_url, relation) tuples for any reftest references specified in + the file""" + rv = [] + rel_map = {"match": "==", "mismatch": "!="} + for item in self.reftest_nodes: + if "href" in item.attrib: + ref_url = urljoin(self.url, item.attrib["href"]) + ref_type = rel_map[item.attrib["rel"]] + rv.append((ref_url, ref_type)) + return rv + + @cached_property + def content_is_ref_node(self): + """Boolean indicating whether the file is a non-leaf node in a reftest + graph (i.e. if it contains any <link rel=[mis]match>""" + return bool(self.references) + + def manifest_items(self): + """List of manifest items corresponding to the file. There is typically one + per test, but in the case of reftests a node may have corresponding manifest + items without being a test itself.""" + + if self.name_is_non_test: + rv = [] + + elif self.name_is_stub: + rv = [Stub(self, self.url)] + + elif self.name_is_manual: + rv = [ManualTest(self, self.url)] + + elif self.name_is_multi_global: + rv = [ + TestharnessTest(self, replace_end(self.url, ".any.js", ".any.html")), + TestharnessTest(self, replace_end(self.url, ".any.js", ".any.worker")), + ] + + elif self.name_is_worker: + rv = [TestharnessTest(self, replace_end(self.url, ".worker.js", ".worker"), + timeout=self.timeout)] + + elif self.name_is_webdriver: + rv = [WebdriverSpecTest(self, self.url)] + + elif self.content_is_testharness: + rv = [] + for variant in self.test_variants: + url = self.url + variant + rv.append(TestharnessTest(self, url, timeout=self.timeout)) + + elif self.content_is_ref_node: + rv = [RefTest(self, self.url, self.references, timeout=self.timeout, + viewport_size=self.viewport_size, dpi=self.dpi)] + + else: + # If nothing else it's a helper file, which we don't have a specific type for + rv = [] + + return rv |