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*\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 """
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 """
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