1164 lines
40 KiB
Python
1164 lines
40 KiB
Python
import hashlib
|
|
import re
|
|
import os
|
|
from collections import deque
|
|
from fnmatch import fnmatch
|
|
from io import BytesIO
|
|
from typing import (Any, BinaryIO, Callable, Deque, Dict, Iterable, List,
|
|
Optional, Pattern, Set, Text, Tuple, TypedDict, Union)
|
|
from urllib.parse import parse_qs, urlparse, urljoin
|
|
|
|
try:
|
|
from xml.etree import cElementTree as ElementTree
|
|
except ImportError:
|
|
from xml.etree import ElementTree as ElementTree # type: ignore
|
|
|
|
import html5lib
|
|
|
|
from . import XMLParser
|
|
from .item import (ConformanceCheckerTest,
|
|
CrashTest,
|
|
ManifestItem,
|
|
ManualTest,
|
|
PrintRefTest,
|
|
RefTest,
|
|
SpecItem,
|
|
SupportFile,
|
|
TestharnessTest,
|
|
VisualTest,
|
|
WebDriverSpecTest)
|
|
from .utils import cached_property
|
|
|
|
# Cannot do `from ..metadata.webfeatures.schema import WEB_FEATURES_YML_FILENAME`
|
|
# because relative import beyond toplevel throws *ImportError*!
|
|
from metadata.webfeatures.schema import WEB_FEATURES_YML_FILENAME # type: ignore
|
|
|
|
wd_pattern = "*.py"
|
|
js_meta_re = re.compile(br"//\s*META:\s*(\w*)=(.*)$")
|
|
python_meta_re = re.compile(br"#\s*META:\s*(\w*)=(.*)$")
|
|
|
|
reference_file_re = re.compile(r'(^|[\-_])(not)?ref[0-9]*([\-_]|$)')
|
|
|
|
space_chars: Text = "".join(html5lib.constants.spaceCharacters) # type: ignore[attr-defined]
|
|
|
|
|
|
def replace_end(s: Text, old: Text, new: Text) -> Text:
|
|
"""
|
|
Given a string `s` that ends with `old`, replace that occurrence of `old`
|
|
with `new`.
|
|
"""
|
|
assert s.endswith(old)
|
|
return s[:-len(old)] + new
|
|
|
|
|
|
def read_script_metadata(f: BinaryIO, regexp: Pattern[bytes]) -> Iterable[Tuple[Text, Text]]:
|
|
"""
|
|
Yields any metadata (pairs of strings) from the file-like object `f`,
|
|
as specified according to a supplied regexp.
|
|
|
|
`regexp` - Regexp containing two groups containing the metadata name and
|
|
value.
|
|
"""
|
|
for line in f:
|
|
assert isinstance(line, bytes), line
|
|
m = regexp.match(line)
|
|
if not m:
|
|
break
|
|
|
|
yield (m.groups()[0].decode("utf8"), m.groups()[1].decode("utf8"))
|
|
|
|
|
|
class VariantData(TypedDict, total=False):
|
|
suffix: str
|
|
force_https: bool
|
|
longhand: Set[str]
|
|
|
|
|
|
_any_variants: Dict[Text, VariantData] = {
|
|
"window": {"suffix": ".any.html"},
|
|
"window-module": {},
|
|
"serviceworker": {"force_https": True},
|
|
"serviceworker-module": {"force_https": True},
|
|
"sharedworker": {},
|
|
"sharedworker-module": {},
|
|
"dedicatedworker": {"suffix": ".any.worker.html"},
|
|
"dedicatedworker-module": {"suffix": ".any.worker-module.html"},
|
|
"worker": {"longhand": {"dedicatedworker", "sharedworker", "serviceworker"}},
|
|
"worker-module": {},
|
|
"shadowrealm-in-window": {},
|
|
"shadowrealm-in-shadowrealm": {},
|
|
"shadowrealm-in-dedicatedworker": {},
|
|
"shadowrealm-in-sharedworker": {},
|
|
"shadowrealm-in-serviceworker": {
|
|
"force_https": True,
|
|
"suffix": ".https.any.shadowrealm-in-serviceworker.html",
|
|
},
|
|
"shadowrealm-in-audioworklet": {
|
|
"force_https": True,
|
|
"suffix": ".https.any.shadowrealm-in-audioworklet.html",
|
|
},
|
|
"shadowrealm": {"longhand": {
|
|
"shadowrealm-in-window",
|
|
"shadowrealm-in-shadowrealm",
|
|
"shadowrealm-in-dedicatedworker",
|
|
"shadowrealm-in-sharedworker",
|
|
"shadowrealm-in-serviceworker",
|
|
"shadowrealm-in-audioworklet",
|
|
}},
|
|
"jsshell": {"suffix": ".any.js"},
|
|
}
|
|
|
|
|
|
def get_any_variants(item: Text) -> Set[Text]:
|
|
"""
|
|
Returns a set of variants (strings) defined by the given keyword.
|
|
"""
|
|
assert isinstance(item, str), item
|
|
|
|
variant = _any_variants.get(item, None)
|
|
if variant is None:
|
|
return set()
|
|
|
|
return variant.get("longhand", {item})
|
|
|
|
|
|
def get_default_any_variants() -> Set[Text]:
|
|
"""
|
|
Returns a set of variants (strings) that will be used by default.
|
|
"""
|
|
return set({"window", "dedicatedworker"})
|
|
|
|
|
|
def parse_variants(value: Text) -> Set[Text]:
|
|
"""
|
|
Returns a set of variants (strings) defined by a comma-separated value.
|
|
"""
|
|
assert isinstance(value, str), value
|
|
|
|
if value == "":
|
|
return get_default_any_variants()
|
|
|
|
globals = set()
|
|
for item in value.split(","):
|
|
item = item.strip()
|
|
globals |= get_any_variants(item)
|
|
return globals
|
|
|
|
|
|
def global_suffixes(value: Text) -> Set[Tuple[Text, bool]]:
|
|
"""
|
|
Yields tuples of the relevant filename suffix (a string) and whether the
|
|
variant is intended to run in a JS shell, for the variants defined by the
|
|
given comma-separated value.
|
|
"""
|
|
assert isinstance(value, str), value
|
|
|
|
rv = set()
|
|
|
|
global_types = parse_variants(value)
|
|
for global_type in global_types:
|
|
variant = _any_variants[global_type]
|
|
suffix = variant.get("suffix", ".any.%s.html" % global_type)
|
|
rv.add((suffix, global_type == "jsshell"))
|
|
|
|
return rv
|
|
|
|
|
|
def global_variant_url(url: Text, suffix: Text) -> Text:
|
|
"""
|
|
Returns a url created from the given url and suffix (all strings).
|
|
"""
|
|
url = url.replace(".any.", ".")
|
|
# If the url must be loaded over https, ensure that it will have
|
|
# the form .https.any.js
|
|
if ".https." in url and suffix.startswith(".https."):
|
|
url = url.replace(".https.", ".")
|
|
elif ".h2." in url and suffix.startswith(".h2."):
|
|
url = url.replace(".h2.", ".")
|
|
return replace_end(url, ".js", suffix)
|
|
|
|
|
|
def _parse_html(f: BinaryIO) -> ElementTree.Element:
|
|
return html5lib.parse(f, treebuilder="etree", useChardet=False)
|
|
|
|
def _parse_xml(f: BinaryIO) -> ElementTree.Element:
|
|
try:
|
|
# raises ValueError with an unsupported encoding,
|
|
# ParseError when there's an undefined entity
|
|
return ElementTree.parse(f).getroot()
|
|
except (ValueError, ElementTree.ParseError):
|
|
f.seek(0)
|
|
return ElementTree.parse(f, XMLParser.XMLParser()).getroot() # type: ignore
|
|
|
|
|
|
class SourceFile:
|
|
parsers: Dict[Text, Callable[[BinaryIO], ElementTree.Element]] = {"html":_parse_html,
|
|
"xhtml":_parse_xml,
|
|
"svg":_parse_xml}
|
|
|
|
root_dir_non_test = {"common"}
|
|
|
|
dir_non_test = {"resources",
|
|
"support",
|
|
"tools"}
|
|
|
|
dir_path_non_test: Set[Tuple[Text, ...]] = {("css21", "archive"),
|
|
("css", "CSS2", "archive"),
|
|
("css", "common")}
|
|
|
|
def __init__(self, tests_root: Text,
|
|
rel_path: Text,
|
|
url_base: Text,
|
|
hash: Optional[Text] = None,
|
|
contents: Optional[bytes] = None) -> None:
|
|
"""Object representing a file in a source tree.
|
|
|
|
:param tests_root: Path to the root of the source tree
|
|
:param rel_path_str: File path relative to tests_root
|
|
:param url_base: Base URL used when converting file paths to urls
|
|
:param contents: Byte array of the contents of the file or ``None``.
|
|
"""
|
|
|
|
assert not os.path.isabs(rel_path), rel_path
|
|
if os.name == "nt":
|
|
# do slash normalization on Windows
|
|
rel_path = rel_path.replace("/", "\\")
|
|
|
|
dir_path, filename = os.path.split(rel_path)
|
|
name, ext = os.path.splitext(filename)
|
|
|
|
type_flag = None
|
|
if "-" in name:
|
|
type_meta = name.rsplit("-", 1)[1].split(".")
|
|
type_flag = type_meta[0]
|
|
meta_flags = type_meta[1:]
|
|
else:
|
|
meta_flags = name.split(".")[1:]
|
|
|
|
self.tests_root: Text = tests_root
|
|
self.rel_path: Text = rel_path
|
|
self.dir_path: Text = dir_path
|
|
self.filename: Text = filename
|
|
self.name: Text = name
|
|
self.ext: Text = ext
|
|
self.type_flag: Optional[Text] = type_flag
|
|
self.meta_flags: Union[List[bytes], List[Text]] = meta_flags
|
|
self.url_base = url_base
|
|
self.contents = contents
|
|
self.items_cache: Optional[Tuple[Text, List[ManifestItem]]] = None
|
|
self._hash = hash
|
|
|
|
def __getstate__(self) -> Dict[str, Any]:
|
|
# Remove computed properties if we pickle this class
|
|
rv = self.__dict__.copy()
|
|
|
|
if "__cached_properties__" in rv:
|
|
cached_properties = rv["__cached_properties__"]
|
|
rv = {key:value for key, value in rv.items() if key not in cached_properties}
|
|
del rv["__cached_properties__"]
|
|
return rv
|
|
|
|
def name_prefix(self, prefix: Text) -> bool:
|
|
"""Check if the filename starts with a given prefix
|
|
|
|
:param prefix: The prefix to check"""
|
|
return self.name.startswith(prefix)
|
|
|
|
def is_dir(self) -> bool:
|
|
"""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) -> BinaryIO:
|
|
"""
|
|
Return either
|
|
* the contents specified in the constructor, if any;
|
|
* a File object opened for reading the file contents.
|
|
"""
|
|
if self.contents is not None:
|
|
file_obj: BinaryIO = BytesIO(self.contents)
|
|
else:
|
|
file_obj = open(self.path, 'rb')
|
|
return file_obj
|
|
|
|
@cached_property
|
|
def rel_path_parts(self) -> Tuple[Text, ...]:
|
|
return tuple(self.rel_path.split(os.path.sep))
|
|
|
|
@cached_property
|
|
def path(self) -> Text:
|
|
return os.path.join(self.tests_root, self.rel_path)
|
|
|
|
@cached_property
|
|
def rel_url(self) -> Text:
|
|
assert not os.path.isabs(self.rel_path), self.rel_path
|
|
return self.rel_path.replace(os.sep, "/")
|
|
|
|
@cached_property
|
|
def url(self) -> Text:
|
|
return urljoin(self.url_base, self.rel_url)
|
|
|
|
@cached_property
|
|
def hash(self) -> Text:
|
|
if not self._hash:
|
|
with self.open() as f:
|
|
content = f.read()
|
|
|
|
data = b"".join((b"blob ", b"%d" % len(content), b"\0", content))
|
|
self._hash = str(hashlib.sha1(data).hexdigest())
|
|
|
|
return self._hash
|
|
|
|
def in_non_test_dir(self) -> bool:
|
|
if self.dir_path == "":
|
|
return True
|
|
|
|
parts = self.rel_path_parts
|
|
|
|
if (parts[0] in self.root_dir_non_test or
|
|
any(item in self.dir_non_test for item in parts) or
|
|
any(parts[:len(path)] == path for path in self.dir_path_non_test)):
|
|
return True
|
|
return False
|
|
|
|
def in_conformance_checker_dir(self) -> bool:
|
|
return self.rel_path_parts[0] == "conformance-checkers"
|
|
|
|
@property
|
|
def name_is_non_test(self) -> bool:
|
|
"""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 == "META.yml" or
|
|
self.filename == WEB_FEATURES_YML_FILENAME or
|
|
self.filename.startswith(".") or
|
|
self.filename.endswith(".headers") or
|
|
self.filename.endswith(".ini") or
|
|
self.in_non_test_dir())
|
|
|
|
@property
|
|
def name_is_conformance(self) -> bool:
|
|
return (self.in_conformance_checker_dir() and
|
|
self.type_flag in ("is-valid", "no-valid"))
|
|
|
|
@property
|
|
def name_is_conformance_support(self) -> bool:
|
|
return self.in_conformance_checker_dir()
|
|
|
|
@property
|
|
def name_is_manual(self) -> bool:
|
|
"""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_visual(self) -> bool:
|
|
"""Check if the file name matches the conditions for the file to
|
|
be a visual test file"""
|
|
return self.type_flag == "visual"
|
|
|
|
@property
|
|
def name_is_multi_global(self) -> bool:
|
|
"""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) -> bool:
|
|
"""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_window(self) -> bool:
|
|
"""Check if the file name matches the conditions for the file to
|
|
be a window js test file"""
|
|
return "window" in self.meta_flags and self.ext == ".js"
|
|
|
|
@property
|
|
def name_is_webdriver(self) -> bool:
|
|
"""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_path_parts = self.rel_path_parts
|
|
return (((rel_path_parts[0] == "webdriver" and len(rel_path_parts) > 1) or
|
|
(rel_path_parts[:2] == ("infrastructure", "webdriver") and
|
|
len(rel_path_parts) > 2)) and
|
|
self.filename not in ("__init__.py", "conftest.py") and
|
|
fnmatch(self.filename, wd_pattern))
|
|
|
|
@property
|
|
def name_is_reference(self) -> bool:
|
|
"""Check if the file name matches the conditions for the file to
|
|
be a reference file (not a reftest)"""
|
|
return "/reference/" in self.url or bool(reference_file_re.search(self.name))
|
|
|
|
@property
|
|
def name_is_crashtest(self) -> bool:
|
|
return (self.markup_type is not None and
|
|
(self.type_flag == "crash" or "crashtests" in self.dir_path.split(os.path.sep)))
|
|
|
|
@property
|
|
def name_is_tentative(self) -> bool:
|
|
"""Check if the file name matches the conditions for the file to be a
|
|
tentative file.
|
|
|
|
See https://web-platform-tests.org/writing-tests/file-names.html#test-features"""
|
|
return "tentative" in self.meta_flags or "tentative" in self.dir_path.split(os.path.sep)
|
|
|
|
@property
|
|
def name_is_print_reftest(self) -> bool:
|
|
return (self.markup_type is not None and
|
|
(self.type_flag == "print" or "print" in self.dir_path.split(os.path.sep)))
|
|
|
|
@property
|
|
def markup_type(self) -> Optional[Text]:
|
|
"""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) -> Optional[ElementTree.Element]:
|
|
"""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
|
|
|
|
return tree
|
|
|
|
@cached_property
|
|
def timeout_nodes(self) -> List[ElementTree.Element]:
|
|
"""List of ElementTree Elements corresponding to nodes in a test that
|
|
specify timeouts"""
|
|
assert self.root is not None
|
|
return self.root.findall(".//{http://www.w3.org/1999/xhtml}meta[@name='timeout']")
|
|
|
|
@cached_property
|
|
def pac_nodes(self) -> List[ElementTree.Element]:
|
|
"""List of ElementTree Elements corresponding to nodes in a test that
|
|
specify PAC (proxy auto-config)"""
|
|
assert self.root is not None
|
|
return self.root.findall(".//{http://www.w3.org/1999/xhtml}meta[@name='pac']")
|
|
|
|
@cached_property
|
|
def script_metadata(self) -> Optional[List[Tuple[Text, Text]]]:
|
|
if self.name_is_worker or self.name_is_multi_global or self.name_is_window:
|
|
regexp = js_meta_re
|
|
elif self.name_is_webdriver:
|
|
regexp = python_meta_re
|
|
else:
|
|
return None
|
|
|
|
with self.open() as f:
|
|
return list(read_script_metadata(f, regexp))
|
|
|
|
@cached_property
|
|
def timeout(self) -> Optional[Text]:
|
|
"""The timeout of a test or reference file. "long" if the file has an extended timeout
|
|
or None otherwise"""
|
|
if self.script_metadata:
|
|
if any(m == ("timeout", "long") for m in self.script_metadata):
|
|
return "long"
|
|
|
|
if self.root is None:
|
|
return None
|
|
|
|
if self.timeout_nodes:
|
|
timeout_str: Optional[Text] = self.timeout_nodes[0].attrib.get("content", None)
|
|
if timeout_str and timeout_str.lower() == "long":
|
|
return "long"
|
|
|
|
return None
|
|
|
|
@cached_property
|
|
def pac(self) -> Optional[Text]:
|
|
"""The PAC (proxy config) of a test or reference file. A URL or null"""
|
|
if self.script_metadata:
|
|
for (meta, content) in self.script_metadata:
|
|
if meta == 'pac':
|
|
return content
|
|
|
|
if self.root is None:
|
|
return None
|
|
|
|
if self.pac_nodes:
|
|
return self.pac_nodes[0].attrib.get("content", None)
|
|
|
|
return None
|
|
|
|
@cached_property
|
|
def viewport_nodes(self) -> List[ElementTree.Element]:
|
|
"""List of ElementTree Elements corresponding to nodes in a test that
|
|
specify viewport sizes"""
|
|
assert self.root is not None
|
|
return self.root.findall(".//{http://www.w3.org/1999/xhtml}meta[@name='viewport-size']")
|
|
|
|
@cached_property
|
|
def viewport_size(self) -> Optional[Text]:
|
|
"""The viewport size of a test or reference file"""
|
|
if self.root is None:
|
|
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[ElementTree.Element]:
|
|
"""List of ElementTree Elements corresponding to nodes in a test that
|
|
specify device pixel ratios"""
|
|
assert self.root is not None
|
|
return self.root.findall(".//{http://www.w3.org/1999/xhtml}meta[@name='device-pixel-ratio']")
|
|
|
|
@cached_property
|
|
def dpi(self) -> Optional[Text]:
|
|
"""The device pixel ratio of a test or reference file"""
|
|
if self.root is None:
|
|
return None
|
|
|
|
if not self.dpi_nodes:
|
|
return None
|
|
|
|
return self.dpi_nodes[0].attrib.get("content", None)
|
|
|
|
def parse_ref_keyed_meta(self, node: ElementTree.Element) -> Tuple[Optional[Tuple[Text, Text, Text]], Text]:
|
|
item: Text = node.attrib.get("content", "")
|
|
|
|
parts = item.rsplit(":", 1)
|
|
if len(parts) == 1:
|
|
key: Optional[Tuple[Text, Text, Text]] = None
|
|
value = parts[0]
|
|
else:
|
|
key_part = urljoin(self.url, parts[0])
|
|
reftype = None
|
|
for ref in self.references: # type: Tuple[Text, Text]
|
|
if ref[0] == key_part:
|
|
reftype = ref[1]
|
|
break
|
|
if reftype not in ("==", "!="):
|
|
raise ValueError("Key %s doesn't correspond to a reference" % key_part)
|
|
key = (self.url, key_part, reftype)
|
|
value = parts[1]
|
|
|
|
return key, value
|
|
|
|
|
|
@cached_property
|
|
def fuzzy_nodes(self) -> List[ElementTree.Element]:
|
|
"""List of ElementTree Elements corresponding to nodes in a test that
|
|
specify reftest fuzziness"""
|
|
assert self.root is not None
|
|
return self.root.findall(".//{http://www.w3.org/1999/xhtml}meta[@name='fuzzy']")
|
|
|
|
|
|
@cached_property
|
|
def fuzzy(self) -> Dict[Optional[Tuple[Text, Text, Text]], List[List[int]]]:
|
|
rv: Dict[Optional[Tuple[Text, Text, Text]], List[List[int]]] = {}
|
|
if self.root is None:
|
|
return rv
|
|
|
|
if not self.fuzzy_nodes:
|
|
return rv
|
|
|
|
args = ["maxDifference", "totalPixels"]
|
|
|
|
for node in self.fuzzy_nodes:
|
|
key, value = self.parse_ref_keyed_meta(node)
|
|
ranges = value.split(";")
|
|
if len(ranges) != 2:
|
|
raise ValueError("Malformed fuzzy value %s" % value)
|
|
arg_values: Dict[Text, List[int]] = {}
|
|
positional_args: Deque[List[int]] = deque()
|
|
for range_str_value in ranges: # type: Text
|
|
name: Optional[Text] = None
|
|
if "=" in range_str_value:
|
|
name, range_str_value = (part.strip()
|
|
for part in range_str_value.split("=", 1))
|
|
if name not in args:
|
|
raise ValueError("%s is not a valid fuzzy property" % name)
|
|
if arg_values.get(name):
|
|
raise ValueError("Got multiple values for argument %s" % name)
|
|
if "-" in range_str_value:
|
|
range_min, range_max = range_str_value.split("-")
|
|
else:
|
|
range_min = range_str_value
|
|
range_max = range_str_value
|
|
try:
|
|
range_value = [int(x.strip()) for x in (range_min, range_max)]
|
|
except ValueError:
|
|
raise ValueError("Fuzzy value %s must be a range of integers" %
|
|
range_str_value)
|
|
if name is None:
|
|
positional_args.append(range_value)
|
|
else:
|
|
arg_values[name] = range_value
|
|
rv[key] = []
|
|
for arg_name in args:
|
|
if arg_values.get(arg_name):
|
|
arg_value = arg_values.pop(arg_name)
|
|
else:
|
|
arg_value = positional_args.popleft()
|
|
rv[key].append(arg_value)
|
|
assert len(arg_values) == 0 and len(positional_args) == 0
|
|
return rv
|
|
|
|
@cached_property
|
|
def page_ranges_nodes(self) -> List[ElementTree.Element]:
|
|
"""List of ElementTree Elements corresponding to nodes in a test that
|
|
specify print-reftest """
|
|
assert self.root is not None
|
|
return self.root.findall(".//{http://www.w3.org/1999/xhtml}meta[@name='reftest-pages']")
|
|
|
|
@cached_property
|
|
def page_ranges(self) -> Dict[Text, List[List[Optional[int]]]]:
|
|
"""List of ElementTree Elements corresponding to nodes in a test that
|
|
specify print-reftest page ranges"""
|
|
rv: Dict[Text, List[List[Optional[int]]]] = {}
|
|
for node in self.page_ranges_nodes:
|
|
key_data, value = self.parse_ref_keyed_meta(node)
|
|
# Just key by url
|
|
if key_data is None:
|
|
key = self.url
|
|
else:
|
|
key = key_data[1]
|
|
if key in rv:
|
|
raise ValueError("Duplicate page-ranges value")
|
|
rv[key] = []
|
|
for range_str in value.split(","):
|
|
range_str = range_str.strip()
|
|
if "-" in range_str:
|
|
range_parts_str = [item.strip() for item in range_str.split("-")]
|
|
try:
|
|
range_parts = [int(item) if item else None for item in range_parts_str]
|
|
except ValueError:
|
|
raise ValueError("Malformed page-range value %s" % range_str)
|
|
if any(item == 0 for item in range_parts):
|
|
raise ValueError("Malformed page-range value %s" % range_str)
|
|
else:
|
|
try:
|
|
range_parts = [int(range_str)]
|
|
except ValueError:
|
|
raise ValueError("Malformed page-range value %s" % range_str)
|
|
rv[key].append(range_parts)
|
|
return rv
|
|
|
|
@cached_property
|
|
def testharness_nodes(self) -> List[ElementTree.Element]:
|
|
"""List of ElementTree Elements corresponding to nodes representing a
|
|
testharness.js script"""
|
|
assert self.root is not None
|
|
return self.root.findall(".//{http://www.w3.org/1999/xhtml}script[@src='/resources/testharness.js']")
|
|
|
|
@cached_property
|
|
def content_is_testharness(self) -> Optional[bool]:
|
|
"""Boolean indicating whether the file content represents a
|
|
testharness.js test"""
|
|
if self.root is None:
|
|
return None
|
|
return bool(self.testharness_nodes)
|
|
|
|
@cached_property
|
|
def variant_nodes(self) -> List[ElementTree.Element]:
|
|
"""List of ElementTree Elements corresponding to nodes representing a
|
|
test variant"""
|
|
assert self.root is not None
|
|
return self.root.findall(".//{http://www.w3.org/1999/xhtml}meta[@name='variant']")
|
|
|
|
@cached_property
|
|
def test_variants(self) -> List[Text]:
|
|
rv: List[Text] = []
|
|
if self.ext == ".js":
|
|
script_metadata = self.script_metadata
|
|
assert script_metadata is not None
|
|
for (key, value) in script_metadata:
|
|
if key == "variant":
|
|
rv.append(value)
|
|
else:
|
|
for element in self.variant_nodes:
|
|
if "content" in element.attrib:
|
|
variant: Text = element.attrib["content"]
|
|
rv.append(variant)
|
|
|
|
for variant in rv:
|
|
if variant != "":
|
|
if variant[0] not in ("#", "?"):
|
|
raise ValueError("Non-empty variant must start with either a ? or a #")
|
|
if len(variant) == 1 or (variant[0] == "?" and variant[1] == "#"):
|
|
raise ValueError("Variants must not have empty fragment or query " +
|
|
"(omit the empty part instead)")
|
|
|
|
if not rv:
|
|
rv = [""]
|
|
|
|
return rv
|
|
|
|
@cached_property
|
|
def testdriver_nodes(self) -> List[ElementTree.Element]:
|
|
"""List of ElementTree Elements corresponding to nodes representing a
|
|
testdriver.js script"""
|
|
assert self.root is not None
|
|
# `xml.etree.ElementTree.findall` has a limited support of xPath, so
|
|
# explicit filter is required.
|
|
return [node for node in
|
|
self.root.findall(".//{http://www.w3.org/1999/xhtml}script")
|
|
if node.attrib.get('src',
|
|
"") == '/resources/testdriver.js' or
|
|
node.attrib.get('src', "").startswith(
|
|
'/resources/testdriver.js?')]
|
|
|
|
@cached_property
|
|
def has_testdriver(self) -> Optional[bool]:
|
|
"""Boolean indicating whether the file content represents a
|
|
testharness.js test"""
|
|
if self.root is None:
|
|
return None
|
|
return bool(self.testdriver_nodes)
|
|
|
|
def ___get_testdriver_include_path(self) -> Optional[str]:
|
|
if self.script_metadata:
|
|
for (meta, content) in self.script_metadata:
|
|
if meta.strip() == 'script' and (
|
|
content == '/resources/testdriver.js' or content.startswith(
|
|
'/resources/testdriver.js?')):
|
|
return content.strip()
|
|
|
|
if self.root is None:
|
|
return None
|
|
|
|
for node in self.testdriver_nodes:
|
|
if "src" in node.attrib:
|
|
return node.attrib.get("src")
|
|
|
|
return None
|
|
|
|
@cached_property
|
|
def testdriver_features(self) -> Optional[List[Text]]:
|
|
"""
|
|
List of requested testdriver features.
|
|
"""
|
|
|
|
testdriver_include_url = self.___get_testdriver_include_path()
|
|
|
|
if testdriver_include_url is None:
|
|
return None
|
|
|
|
# Parse the URL
|
|
parsed_url = urlparse(testdriver_include_url)
|
|
# Extract query parameters
|
|
query_params = parse_qs(parsed_url.query)
|
|
# Get the values for the 'feature' parameter
|
|
feature_values = query_params.get('feature', [])
|
|
|
|
if len(feature_values) > 0:
|
|
return feature_values
|
|
|
|
return None
|
|
|
|
@cached_property
|
|
def reftest_nodes(self) -> List[ElementTree.Element]:
|
|
"""List of ElementTree Elements corresponding to nodes representing a
|
|
to a reftest <link>"""
|
|
if self.root is None:
|
|
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[Tuple[Text, Text]]:
|
|
"""List of (ref_url, relation) tuples for any reftest references specified in
|
|
the file"""
|
|
rv: List[Tuple[Text, Text]] = []
|
|
rel_map = {"match": "==", "mismatch": "!="}
|
|
for item in self.reftest_nodes:
|
|
if "href" in item.attrib:
|
|
ref_url = urljoin(self.url, item.attrib["href"].strip(space_chars))
|
|
ref_type = rel_map[item.attrib["rel"]]
|
|
rv.append((ref_url, ref_type))
|
|
return rv
|
|
|
|
@cached_property
|
|
def content_is_ref_node(self) -> bool:
|
|
"""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)
|
|
|
|
@cached_property
|
|
def css_flag_nodes(self) -> List[ElementTree.Element]:
|
|
"""List of ElementTree Elements corresponding to nodes representing a
|
|
flag <meta>"""
|
|
if self.root is None:
|
|
return []
|
|
return self.root.findall(".//{http://www.w3.org/1999/xhtml}meta[@name='flags']")
|
|
|
|
@cached_property
|
|
def css_flags(self) -> Set[Text]:
|
|
"""Set of flags specified in the file"""
|
|
rv: Set[Text] = set()
|
|
for item in self.css_flag_nodes:
|
|
if "content" in item.attrib:
|
|
for flag in item.attrib["content"].split():
|
|
rv.add(flag)
|
|
return rv
|
|
|
|
@cached_property
|
|
def content_is_css_manual(self) -> Optional[bool]:
|
|
"""Boolean indicating whether the file content represents a
|
|
CSS WG-style manual test"""
|
|
if self.root is None:
|
|
return None
|
|
# return True if the intersection between the two sets is non-empty
|
|
return bool(self.css_flags & {"animated", "font", "history", "interact", "paged", "speech", "userstyle"})
|
|
|
|
@cached_property
|
|
def spec_link_nodes(self) -> List[ElementTree.Element]:
|
|
"""List of ElementTree Elements corresponding to nodes representing a
|
|
<link rel=help>, used to point to specs"""
|
|
if self.root is None:
|
|
return []
|
|
return self.root.findall(".//{http://www.w3.org/1999/xhtml}link[@rel='help']")
|
|
|
|
@cached_property
|
|
def spec_links(self) -> Set[Text]:
|
|
"""Set of spec links specified in the file"""
|
|
rv: Set[Text] = set()
|
|
for item in self.spec_link_nodes:
|
|
if "href" in item.attrib:
|
|
rv.add(item.attrib["href"].strip(space_chars))
|
|
return rv
|
|
|
|
@cached_property
|
|
def content_is_css_visual(self) -> Optional[bool]:
|
|
"""Boolean indicating whether the file content represents a
|
|
CSS WG-style visual test"""
|
|
if self.root is None:
|
|
return None
|
|
return bool(self.ext in {'.xht', '.html', '.xhtml', '.htm', '.xml', '.svg'} and
|
|
self.spec_links)
|
|
|
|
@property
|
|
def type(self) -> Text:
|
|
possible_types = self.possible_types
|
|
if len(possible_types) == 1:
|
|
return possible_types.pop()
|
|
|
|
rv, _ = self.manifest_items()
|
|
return rv
|
|
|
|
@property
|
|
def possible_types(self) -> Set[Text]:
|
|
"""Determines the set of possible types without reading the file"""
|
|
|
|
if self.items_cache:
|
|
return {self.items_cache[0]}
|
|
|
|
if self.name_is_non_test:
|
|
return {SupportFile.item_type}
|
|
|
|
if self.name_is_manual:
|
|
return {ManualTest.item_type}
|
|
|
|
if self.name_is_conformance:
|
|
return {ConformanceCheckerTest.item_type}
|
|
|
|
if self.name_is_conformance_support:
|
|
return {SupportFile.item_type}
|
|
|
|
if self.name_is_webdriver:
|
|
return {WebDriverSpecTest.item_type}
|
|
|
|
if self.name_is_visual:
|
|
return {VisualTest.item_type}
|
|
|
|
if self.name_is_crashtest:
|
|
return {CrashTest.item_type}
|
|
|
|
if self.name_is_print_reftest:
|
|
return {PrintRefTest.item_type}
|
|
|
|
if self.name_is_multi_global:
|
|
return {TestharnessTest.item_type}
|
|
|
|
if self.name_is_worker:
|
|
return {TestharnessTest.item_type}
|
|
|
|
if self.name_is_window:
|
|
return {TestharnessTest.item_type}
|
|
|
|
if self.markup_type is None:
|
|
return {SupportFile.item_type}
|
|
|
|
if not self.name_is_reference:
|
|
return {ManualTest.item_type,
|
|
TestharnessTest.item_type,
|
|
RefTest.item_type,
|
|
VisualTest.item_type,
|
|
SupportFile.item_type}
|
|
|
|
return {TestharnessTest.item_type,
|
|
RefTest.item_type,
|
|
SupportFile.item_type}
|
|
|
|
def manifest_items(self) -> Tuple[Text, List[ManifestItem]]:
|
|
"""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.items_cache:
|
|
return self.items_cache
|
|
|
|
drop_cached = "root" not in self.__dict__
|
|
|
|
if self.name_is_non_test:
|
|
rv: Tuple[Text, List[ManifestItem]] = ("support", [
|
|
SupportFile(
|
|
self.tests_root,
|
|
self.rel_path
|
|
)])
|
|
|
|
elif self.name_is_manual:
|
|
rv = ManualTest.item_type, [
|
|
ManualTest(
|
|
self.tests_root,
|
|
self.rel_path,
|
|
self.url_base,
|
|
self.rel_url
|
|
)]
|
|
|
|
elif self.name_is_conformance:
|
|
rv = ConformanceCheckerTest.item_type, [
|
|
ConformanceCheckerTest(
|
|
self.tests_root,
|
|
self.rel_path,
|
|
self.url_base,
|
|
self.rel_url
|
|
)]
|
|
|
|
elif self.name_is_conformance_support:
|
|
rv = "support", [
|
|
SupportFile(
|
|
self.tests_root,
|
|
self.rel_path
|
|
)]
|
|
|
|
elif self.name_is_webdriver:
|
|
rv = WebDriverSpecTest.item_type, [
|
|
WebDriverSpecTest(
|
|
self.tests_root,
|
|
self.rel_path,
|
|
self.url_base,
|
|
self.rel_url,
|
|
timeout=self.timeout
|
|
)]
|
|
|
|
elif self.name_is_visual:
|
|
rv = VisualTest.item_type, [
|
|
VisualTest(
|
|
self.tests_root,
|
|
self.rel_path,
|
|
self.url_base,
|
|
self.rel_url
|
|
)]
|
|
|
|
elif self.name_is_crashtest:
|
|
rv = CrashTest.item_type, [
|
|
CrashTest(
|
|
self.tests_root,
|
|
self.rel_path,
|
|
self.url_base,
|
|
self.rel_url,
|
|
testdriver=self.has_testdriver,
|
|
)]
|
|
|
|
elif self.name_is_print_reftest:
|
|
references = self.references
|
|
if not references:
|
|
raise ValueError("%s detected as print reftest but doesn't have any refs" %
|
|
self.path)
|
|
rv = PrintRefTest.item_type, [
|
|
PrintRefTest(
|
|
self.tests_root,
|
|
self.rel_path,
|
|
self.url_base,
|
|
self.rel_url,
|
|
references=references,
|
|
timeout=self.timeout,
|
|
viewport_size=self.viewport_size,
|
|
fuzzy=self.fuzzy,
|
|
page_ranges=self.page_ranges,
|
|
testdriver=self.has_testdriver,
|
|
)]
|
|
|
|
elif self.name_is_multi_global:
|
|
globals = ""
|
|
script_metadata = self.script_metadata
|
|
assert script_metadata is not None
|
|
for (key, value) in script_metadata:
|
|
if key == "global":
|
|
globals = value
|
|
break
|
|
|
|
tests: List[ManifestItem] = [
|
|
TestharnessTest(
|
|
self.tests_root,
|
|
self.rel_path,
|
|
self.url_base,
|
|
global_variant_url(self.rel_url, suffix) + variant,
|
|
timeout=self.timeout,
|
|
pac=self.pac,
|
|
testdriver_features=self.testdriver_features,
|
|
jsshell=jsshell,
|
|
script_metadata=self.script_metadata
|
|
)
|
|
for (suffix, jsshell) in sorted(global_suffixes(globals))
|
|
for variant in self.test_variants
|
|
]
|
|
rv = TestharnessTest.item_type, tests
|
|
|
|
elif self.name_is_worker:
|
|
test_url = replace_end(self.rel_url, ".worker.js", ".worker.html")
|
|
tests = [
|
|
TestharnessTest(
|
|
self.tests_root,
|
|
self.rel_path,
|
|
self.url_base,
|
|
test_url + variant,
|
|
timeout=self.timeout,
|
|
pac=self.pac,
|
|
testdriver_features=self.testdriver_features,
|
|
script_metadata=self.script_metadata
|
|
)
|
|
for variant in self.test_variants
|
|
]
|
|
rv = TestharnessTest.item_type, tests
|
|
|
|
elif self.name_is_window:
|
|
test_url = replace_end(self.rel_url, ".window.js", ".window.html")
|
|
tests = [
|
|
TestharnessTest(
|
|
self.tests_root,
|
|
self.rel_path,
|
|
self.url_base,
|
|
test_url + variant,
|
|
timeout=self.timeout,
|
|
pac=self.pac,
|
|
testdriver_features=self.testdriver_features,
|
|
script_metadata=self.script_metadata
|
|
)
|
|
for variant in self.test_variants
|
|
]
|
|
rv = TestharnessTest.item_type, tests
|
|
|
|
elif self.content_is_css_manual and not self.name_is_reference:
|
|
rv = ManualTest.item_type, [
|
|
ManualTest(
|
|
self.tests_root,
|
|
self.rel_path,
|
|
self.url_base,
|
|
self.rel_url
|
|
)]
|
|
|
|
elif self.content_is_testharness:
|
|
rv = TestharnessTest.item_type, []
|
|
testdriver = self.has_testdriver
|
|
for variant in self.test_variants:
|
|
url = self.rel_url + variant
|
|
rv[1].append(TestharnessTest(
|
|
self.tests_root,
|
|
self.rel_path,
|
|
self.url_base,
|
|
url,
|
|
timeout=self.timeout,
|
|
pac=self.pac,
|
|
testdriver_features=self.testdriver_features,
|
|
testdriver=testdriver,
|
|
script_metadata=self.script_metadata
|
|
))
|
|
|
|
elif self.content_is_ref_node:
|
|
rv = RefTest.item_type, []
|
|
for variant in self.test_variants:
|
|
url = self.rel_url + variant
|
|
rv[1].append(RefTest(
|
|
self.tests_root,
|
|
self.rel_path,
|
|
self.url_base,
|
|
url,
|
|
references=[
|
|
(ref[0] + variant, ref[1])
|
|
for ref in self.references
|
|
],
|
|
timeout=self.timeout,
|
|
viewport_size=self.viewport_size,
|
|
dpi=self.dpi,
|
|
fuzzy=self.fuzzy,
|
|
testdriver=self.has_testdriver,
|
|
))
|
|
|
|
elif self.content_is_css_visual and not self.name_is_reference:
|
|
rv = VisualTest.item_type, [
|
|
VisualTest(
|
|
self.tests_root,
|
|
self.rel_path,
|
|
self.url_base,
|
|
self.rel_url
|
|
)]
|
|
|
|
else:
|
|
rv = "support", [
|
|
SupportFile(
|
|
self.tests_root,
|
|
self.rel_path
|
|
)]
|
|
|
|
assert rv[0] in self.possible_types
|
|
assert len(rv[1]) == len(set(rv[1]))
|
|
|
|
self.items_cache = rv
|
|
|
|
if drop_cached and "__cached_properties__" in self.__dict__:
|
|
cached_properties = self.__dict__["__cached_properties__"]
|
|
for prop in cached_properties:
|
|
if prop in self.__dict__:
|
|
del self.__dict__[prop]
|
|
del self.__dict__["__cached_properties__"]
|
|
|
|
return rv
|
|
|
|
def manifest_spec_items(self) -> Optional[Tuple[Text, List[ManifestItem]]]:
|
|
specs = list(self.spec_links)
|
|
if not specs:
|
|
return None
|
|
rv: Tuple[Text, List[ManifestItem]] = (SpecItem.item_type, [
|
|
SpecItem(
|
|
self.tests_root,
|
|
self.rel_path,
|
|
specs
|
|
)])
|
|
return rv
|