diff options
Diffstat (limited to 'testing/web-platform/tests/tools/manifest/manifest.py')
-rw-r--r-- | testing/web-platform/tests/tools/manifest/manifest.py | 418 |
1 files changed, 418 insertions, 0 deletions
diff --git a/testing/web-platform/tests/tools/manifest/manifest.py b/testing/web-platform/tests/tools/manifest/manifest.py new file mode 100644 index 000000000..80fe70f95 --- /dev/null +++ b/testing/web-platform/tests/tools/manifest/manifest.py @@ -0,0 +1,418 @@ +import json +import os +from collections import defaultdict, OrderedDict +from six import iteritems + +from .item import item_types, ManualTest, WebdriverSpecTest, Stub, RefTest, TestharnessTest +from .log import get_logger +from .sourcefile import SourceFile +from .utils import from_os_path, to_os_path + + +CURRENT_VERSION = 3 + + +class ManifestError(Exception): + pass + + +class ManifestVersionMismatch(ManifestError): + pass + +class Manifest(object): + def __init__(self, git_rev=None, url_base="/"): + # Dict of item_type: {path: set(manifest_items)} + self._data = dict((item_type, defaultdict(set)) + for item_type in item_types) + self.rev = git_rev + self.url_base = url_base + self.local_changes = LocalChanges(self) + # reftest nodes arranged as {path: set(manifest_items)} + self.reftest_nodes = defaultdict(set) + self.reftest_nodes_by_url = {} + + def _included_items(self, include_types=None): + if include_types is None: + include_types = item_types + + for item_type in include_types: + paths = self._data[item_type].copy() + for local_types, local_paths in self.local_changes.itertypes(item_type): + for path, items in iteritems(local_paths): + paths[path] = items + for path in self.local_changes.iterdeleted(): + if path in paths: + del paths[path] + if item_type == "reftest": + for path, items in self.local_changes.iterdeletedreftests(): + paths[path] -= items + if len(paths[path]) == 0: + del paths[path] + + yield item_type, paths + + def contains_path(self, path): + return any(path in paths for _, paths in self._included_items()) + + def add(self, item): + if item is None: + return + + if isinstance(item, RefTest): + self.reftest_nodes[item.path].add(item) + self.reftest_nodes_by_url[item.url] = item + else: + self._add(item) + + item.manifest = self + + def _add(self, item): + self._data[item.item_type][item.path].add(item) + + def extend(self, items): + for item in items: + self.add(item) + + def remove_path(self, path): + for item_type in item_types: + if path in self._data[item_type]: + del self._data[item_type][path] + + def itertypes(self, *types): + if not types: + types = None + for item_type, items in self._included_items(types): + for item in sorted(iteritems(items)): + yield item + + def __iter__(self): + for item in self.itertypes(): + yield item + + def __getitem__(self, path): + for _, paths in self._included_items(): + if path in paths: + return paths[path] + raise KeyError + + def get_reference(self, url): + if url in self.local_changes.reftest_nodes_by_url: + return self.local_changes.reftest_nodes_by_url[url] + + if url in self.reftest_nodes_by_url: + return self.reftest_nodes_by_url[url] + + return None + + def _committed_with_path(self, rel_path): + rv = set() + + for paths_items in self._data.itervalues(): + rv |= paths_items.get(rel_path, set()) + + if rel_path in self.reftest_nodes: + rv |= self.reftest_nodes[rel_path] + + return rv + + def _committed_paths(self): + rv = set() + for paths_items in self._data.itervalues(): + rv |= set(paths_items.keys()) + return rv + + def update(self, + tests_root, + url_base, + new_rev, + committed_changes=None, + local_changes=None, + remove_missing_local=False): + + if local_changes is None: + local_changes = {} + + if committed_changes is not None: + for rel_path, status in committed_changes: + self.remove_path(rel_path) + if status == "modified": + use_committed = rel_path in local_changes + source_file = SourceFile(tests_root, + rel_path, + url_base, + use_committed=use_committed) + self.extend(source_file.manifest_items()) + + self.local_changes = LocalChanges(self) + + local_paths = set() + for rel_path, status in iteritems(local_changes): + local_paths.add(rel_path) + + if status == "modified": + existing_items = self._committed_with_path(rel_path) + source_file = SourceFile(tests_root, + rel_path, + url_base, + use_committed=False) + local_items = set(source_file.manifest_items()) + + updated_items = local_items - existing_items + self.local_changes.extend(updated_items) + else: + self.local_changes.add_deleted(rel_path) + + if remove_missing_local: + for path in self._committed_paths() - local_paths: + self.local_changes.add_deleted(path) + + self.update_reftests() + + if new_rev is not None: + self.rev = new_rev + self.url_base = url_base + + def update_reftests(self): + default_reftests = self.compute_reftests(self.reftest_nodes) + all_reftest_nodes = self.reftest_nodes.copy() + all_reftest_nodes.update(self.local_changes.reftest_nodes) + + for item in self.local_changes.iterdeleted(): + if item in all_reftest_nodes: + del all_reftest_nodes[item] + + modified_reftests = self.compute_reftests(all_reftest_nodes) + + added_reftests = modified_reftests - default_reftests + # The interesting case here is not when the file is deleted, + # but when a reftest like A == B is changed to the form + # C == A == B, so that A still exists but is now a ref rather than + # a test. + removed_reftests = default_reftests - modified_reftests + + dests = [(default_reftests, self._data["reftest"]), + (added_reftests, self.local_changes._data["reftest"]), + (removed_reftests, self.local_changes._deleted_reftests)] + + #TODO: Warn if there exist unreachable reftest nodes + for source, target in dests: + for item in source: + target[item.path].add(item) + + def compute_reftests(self, reftest_nodes): + """Given a set of reftest_nodes, return a set of all the nodes that are top-level + tests i.e. don't have any incoming reference links.""" + + reftests = set() + + has_inbound = set() + for path, items in iteritems(reftest_nodes): + for item in items: + for ref_url, ref_type in item.references: + has_inbound.add(ref_url) + + for path, items in iteritems(reftest_nodes): + for item in items: + if item.url in has_inbound: + continue + reftests.add(item) + + return reftests + + def to_json(self): + out_items = { + item_type: sorted( + test.to_json() + for _, tests in iteritems(items) + for test in tests + ) + for item_type, items in iteritems(self._data) + } + + reftest_nodes = OrderedDict() + for key, value in sorted(iteritems(self.reftest_nodes)): + reftest_nodes[from_os_path(key)] = [v.to_json() for v in value] + + rv = {"url_base": self.url_base, + "rev": self.rev, + "local_changes": self.local_changes.to_json(), + "items": out_items, + "reftest_nodes": reftest_nodes, + "version": CURRENT_VERSION} + return rv + + @classmethod + def from_json(cls, tests_root, obj): + version = obj.get("version") + if version != CURRENT_VERSION: + raise ManifestVersionMismatch + + self = cls(git_rev=obj["rev"], + url_base=obj.get("url_base", "/")) + if not hasattr(obj, "items"): + raise ManifestError + + item_classes = {"testharness": TestharnessTest, + "reftest": RefTest, + "manual": ManualTest, + "stub": Stub, + "wdspec": WebdriverSpecTest} + + source_files = {} + + for k, values in iteritems(obj["items"]): + if k not in item_types: + raise ManifestError + for v in values: + manifest_item = item_classes[k].from_json(self, tests_root, v, + source_files=source_files) + self._add(manifest_item) + + for path, values in iteritems(obj["reftest_nodes"]): + path = to_os_path(path) + for v in values: + item = RefTest.from_json(self, tests_root, v, + source_files=source_files) + self.reftest_nodes[path].add(item) + self.reftest_nodes_by_url[v["url"]] = item + + self.local_changes = LocalChanges.from_json(self, + tests_root, + obj["local_changes"], + source_files=source_files) + self.update_reftests() + return self + + +class LocalChanges(object): + def __init__(self, manifest): + self.manifest = manifest + self._data = dict((item_type, defaultdict(set)) for item_type in item_types) + self._deleted = set() + self.reftest_nodes = defaultdict(set) + self.reftest_nodes_by_url = {} + self._deleted_reftests = defaultdict(set) + + def add(self, item): + if item is None: + return + + if isinstance(item, RefTest): + self.reftest_nodes[item.path].add(item) + self.reftest_nodes_by_url[item.url] = item + else: + self._add(item) + + item.manifest = self.manifest + + def _add(self, item): + self._data[item.item_type][item.path].add(item) + + def extend(self, items): + for item in items: + self.add(item) + + def add_deleted(self, path): + self._deleted.add(path) + + def is_deleted(self, path): + return path in self._deleted + + def itertypes(self, *types): + for item_type in types: + yield item_type, self._data[item_type] + + def iterdeleted(self): + for item in self._deleted: + yield item + + def iterdeletedreftests(self): + for item in iteritems(self._deleted_reftests): + yield item + + def __getitem__(self, item_type): + return self._data[item_type] + + def to_json(self): + reftest_nodes = {from_os_path(key): [v.to_json() for v in value] + for key, value in iteritems(self.reftest_nodes)} + + deleted_reftests = {from_os_path(key): [v.to_json() for v in value] + for key, value in iteritems(self._deleted_reftests)} + + rv = {"items": defaultdict(dict), + "reftest_nodes": reftest_nodes, + "deleted": [from_os_path(path) for path in self._deleted], + "deleted_reftests": deleted_reftests} + + for test_type, paths in iteritems(self._data): + for path, tests in iteritems(paths): + path = from_os_path(path) + rv["items"][test_type][path] = [test.to_json() for test in tests] + + return rv + + @classmethod + def from_json(cls, manifest, tests_root, obj, source_files=None): + self = cls(manifest) + if not hasattr(obj, "items"): + raise ManifestError + + item_classes = {"testharness": TestharnessTest, + "reftest": RefTest, + "manual": ManualTest, + "stub": Stub, + "wdspec": WebdriverSpecTest} + + for test_type, paths in iteritems(obj["items"]): + for path, tests in iteritems(paths): + for test in tests: + manifest_item = item_classes[test_type].from_json(manifest, + tests_root, + test, + source_files=source_files) + self.add(manifest_item) + + for path, values in iteritems(obj["reftest_nodes"]): + path = to_os_path(path) + for v in values: + item = RefTest.from_json(self.manifest, tests_root, v, + source_files=source_files) + self.reftest_nodes[path].add(item) + self.reftest_nodes_by_url[item.url] = item + + for item in obj["deleted"]: + self.add_deleted(to_os_path(item)) + + for path, values in iteritems(obj.get("deleted_reftests", {})): + path = to_os_path(path) + for v in values: + item = RefTest.from_json(self.manifest, tests_root, v, + source_files=source_files) + self._deleted_reftests[path].add(item) + + return self + +def load(tests_root, manifest): + logger = get_logger() + + # "manifest" is a path or file-like object. + if isinstance(manifest, basestring): + if os.path.exists(manifest): + logger.debug("Opening manifest at %s" % manifest) + else: + logger.debug("Creating new manifest at %s" % manifest) + try: + with open(manifest) as f: + rv = Manifest.from_json(tests_root, json.load(f)) + except IOError: + rv = Manifest(None) + return rv + + return Manifest.from_json(tests_root, json.load(manifest)) + + +def write(manifest, manifest_path): + with open(manifest_path, "wb") as f: + json.dump(manifest.to_json(), f, sort_keys=True, indent=2, separators=(',', ': ')) + f.write("\n") |