| from __future__ import print_function |
| import array |
| import os |
| import shutil |
| import tempfile |
| import uuid |
| from collections import defaultdict, namedtuple |
| |
| from mozlog import structuredlog |
| |
| import manifestupdate |
| import testloader |
| import wptmanifest |
| import wpttest |
| from expected import expected_path |
| from vcs import git |
| manifest = None # Module that will be imported relative to test_root |
| manifestitem = None |
| |
| logger = structuredlog.StructuredLogger("web-platform-tests") |
| |
| try: |
| import ujson as json |
| except ImportError: |
| import json |
| |
| |
| def update_expected(test_paths, serve_root, log_file_names, |
| rev_old=None, rev_new="HEAD", ignore_existing=False, |
| sync_root=None, property_order=None, boolean_properties=None, |
| stability=None): |
| """Update the metadata files for web-platform-tests based on |
| the results obtained in a previous run or runs |
| |
| If stability is not None, assume log_file_names refers to logs from repeated |
| test jobs, disable tests that don't behave as expected on all runs""" |
| do_delayed_imports(serve_root) |
| |
| id_test_map = load_test_data(test_paths) |
| |
| for metadata_path, updated_ini in update_from_logs(id_test_map, |
| *log_file_names, |
| ignore_existing=ignore_existing, |
| property_order=property_order, |
| boolean_properties=boolean_properties, |
| stability=stability): |
| |
| write_new_expected(metadata_path, updated_ini) |
| if stability: |
| for test in updated_ini.iterchildren(): |
| for subtest in test.iterchildren(): |
| if subtest.new_disabled: |
| print("disabled: %s" % os.path.dirname(subtest.root.test_path) + "/" + subtest.name) |
| if test.new_disabled: |
| print("disabled: %s" % test.root.test_path) |
| |
| |
| def do_delayed_imports(serve_root): |
| global manifest, manifestitem |
| from manifest import manifest, item as manifestitem |
| |
| |
| def files_in_repo(repo_root): |
| return git("ls-tree", "-r", "--name-only", "HEAD").split("\n") |
| |
| |
| def rev_range(rev_old, rev_new, symmetric=False): |
| joiner = ".." if not symmetric else "..." |
| return "".join([rev_old, joiner, rev_new]) |
| |
| |
| def paths_changed(rev_old, rev_new, repo): |
| data = git("diff", "--name-status", rev_range(rev_old, rev_new), repo=repo) |
| lines = [tuple(item.strip() for item in line.strip().split("\t", 1)) |
| for line in data.split("\n") if line.strip()] |
| output = set(lines) |
| return output |
| |
| |
| def load_change_data(rev_old, rev_new, repo): |
| changes = paths_changed(rev_old, rev_new, repo) |
| rv = {} |
| status_keys = {"M": "modified", |
| "A": "new", |
| "D": "deleted"} |
| # TODO: deal with renames |
| for item in changes: |
| rv[item[1]] = status_keys[item[0]] |
| return rv |
| |
| |
| def unexpected_changes(manifests, change_data, files_changed): |
| files_changed = set(files_changed) |
| |
| root_manifest = None |
| for manifest, paths in manifests.iteritems(): |
| if paths["url_base"] == "/": |
| root_manifest = manifest |
| break |
| else: |
| return [] |
| |
| return [fn for _, fn, _ in root_manifest if fn in files_changed and change_data.get(fn) != "M"] |
| |
| # For each testrun |
| # Load all files and scan for the suite_start entry |
| # Build a hash of filename: properties |
| # For each different set of properties, gather all chunks |
| # For each chunk in the set of chunks, go through all tests |
| # for each test, make a map of {conditionals: [(platform, new_value)]} |
| # Repeat for each platform |
| # For each test in the list of tests: |
| # for each conditional: |
| # If all the new values match (or there aren't any) retain that conditional |
| # If any new values mismatch: |
| # If stability and any repeated values don't match, disable the test |
| # else mark the test as needing human attention |
| # Check if all the RHS values are the same; if so collapse the conditionals |
| |
| |
| class InternedData(object): |
| """Class for interning data of any (hashable) type. |
| |
| This class is intended for building a mapping of int <=> value, such |
| that the integer may be stored as a proxy for the real value, and then |
| the real value obtained later from the proxy value. |
| |
| In order to support the use case of packing the integer value as binary, |
| it is possible to specify a maximum bitsize of the data; adding more items |
| than this allowed will result in a ValueError exception. |
| |
| The zero value is reserved to use as a sentinal.""" |
| |
| type_conv = None |
| rev_type_conv = None |
| |
| def __init__(self, max_bits=8): |
| self.max_idx = 2**max_bits - 2 |
| # Reserve 0 as a sentinal |
| self._data = [None], {} |
| |
| def store(self, obj): |
| if self.type_conv is not None: |
| obj = self.type_conv(obj) |
| |
| objs, obj_to_idx = self._data |
| if obj not in obj_to_idx: |
| value = len(objs) |
| objs.append(obj) |
| obj_to_idx[obj] = value |
| if value > self.max_idx: |
| raise ValueError |
| else: |
| value = obj_to_idx[obj] |
| return value |
| |
| def get(self, idx): |
| obj = self._data[0][idx] |
| if self.rev_type_conv is not None: |
| obj = self.rev_type_conv(obj) |
| return obj |
| |
| |
| class RunInfoInterned(InternedData): |
| def type_conv(self, value): |
| return tuple(value.items()) |
| |
| def rev_type_conv(self, value): |
| return dict(value) |
| |
| |
| prop_intern = InternedData(4) |
| run_info_intern = RunInfoInterned() |
| status_intern = InternedData(4) |
| |
| |
| def load_test_data(test_paths): |
| manifest_loader = testloader.ManifestLoader(test_paths, False) |
| manifests = manifest_loader.load() |
| |
| id_test_map = {} |
| for test_manifest, paths in manifests.iteritems(): |
| id_test_map.update(create_test_tree(paths["metadata_path"], |
| test_manifest)) |
| return id_test_map |
| |
| |
| def update_from_logs(id_test_map, *log_filenames, **kwargs): |
| ignore_existing = kwargs.get("ignore_existing", False) |
| property_order = kwargs.get("property_order") |
| boolean_properties = kwargs.get("boolean_properties") |
| stability = kwargs.get("stability") |
| |
| updater = ExpectedUpdater(id_test_map, |
| ignore_existing=ignore_existing) |
| |
| for i, log_filename in enumerate(log_filenames): |
| print("Processing log %d/%d" % (i + 1, len(log_filenames))) |
| with open(log_filename) as f: |
| updater.update_from_log(f) |
| |
| for item in update_results(id_test_map, property_order, boolean_properties, stability): |
| yield item |
| |
| |
| def update_results(id_test_map, property_order, boolean_properties, stability): |
| test_file_items = set(id_test_map.itervalues()) |
| |
| default_expected_by_type = {} |
| for test_type, test_cls in wpttest.manifest_test_cls.iteritems(): |
| if test_cls.result_cls: |
| default_expected_by_type[(test_type, False)] = test_cls.result_cls.default_expected |
| if test_cls.subtest_result_cls: |
| default_expected_by_type[(test_type, True)] = test_cls.subtest_result_cls.default_expected |
| |
| for test_file in test_file_items: |
| updated_expected = test_file.update(property_order, boolean_properties, stability, |
| default_expected_by_type) |
| if updated_expected is not None and updated_expected.modified: |
| yield test_file.metadata_path, updated_expected |
| |
| |
| def directory_manifests(metadata_path): |
| rv = [] |
| for dirpath, dirname, filenames in os.walk(metadata_path): |
| if "__dir__.ini" in filenames: |
| rel_path = os.path.relpath(dirpath, metadata_path) |
| rv.append(os.path.join(rel_path, "__dir__.ini")) |
| return rv |
| |
| |
| def write_changes(metadata_path, expected): |
| # First write the new manifest files to a temporary directory |
| temp_path = tempfile.mkdtemp(dir=os.path.split(metadata_path)[0]) |
| write_new_expected(temp_path, expected) |
| |
| # Copy all files in the root to the temporary location since |
| # these cannot be ini files |
| keep_files = [item for item in os.listdir(metadata_path) if |
| not os.path.isdir(os.path.join(metadata_path, item))] |
| |
| for item in keep_files: |
| dest_dir = os.path.dirname(os.path.join(temp_path, item)) |
| if not os.path.exists(dest_dir): |
| os.makedirs(dest_dir) |
| shutil.copyfile(os.path.join(metadata_path, item), |
| os.path.join(temp_path, item)) |
| |
| # Then move the old manifest files to a new location |
| temp_path_2 = metadata_path + str(uuid.uuid4()) |
| os.rename(metadata_path, temp_path_2) |
| # Move the new files to the destination location and remove the old files |
| os.rename(temp_path, metadata_path) |
| shutil.rmtree(temp_path_2) |
| |
| |
| def write_new_expected(metadata_path, expected): |
| # Serialize the data back to a file |
| path = expected_path(metadata_path, expected.test_path) |
| if not expected.is_empty: |
| manifest_str = wptmanifest.serialize(expected.node, skip_empty_data=True) |
| assert manifest_str != "" |
| dir = os.path.split(path)[0] |
| if not os.path.exists(dir): |
| os.makedirs(dir) |
| tmp_path = path + ".tmp" |
| try: |
| with open(tmp_path, "wb") as f: |
| f.write(manifest_str) |
| os.rename(tmp_path, path) |
| except (Exception, KeyboardInterrupt): |
| try: |
| os.unlink(tmp_path) |
| except OSError: |
| pass |
| else: |
| try: |
| os.unlink(path) |
| except OSError: |
| pass |
| |
| |
| class ExpectedUpdater(object): |
| def __init__(self, id_test_map, ignore_existing=False): |
| self.id_test_map = id_test_map |
| self.ignore_existing = ignore_existing |
| self.run_info = None |
| self.action_map = {"suite_start": self.suite_start, |
| "test_start": self.test_start, |
| "test_status": self.test_status, |
| "test_end": self.test_end, |
| "assertion_count": self.assertion_count, |
| "lsan_leak": self.lsan_leak, |
| "mozleak_object": self.mozleak_object, |
| "mozleak_total": self.mozleak_total} |
| self.tests_visited = {} |
| |
| def update_from_log(self, log_file): |
| self.run_info = None |
| try: |
| data = json.load(log_file) |
| except Exception: |
| pass |
| else: |
| if "action" not in data and "results" in data: |
| self.update_from_wptreport_log(data) |
| return |
| |
| log_file.seek(0) |
| self.update_from_raw_log(log_file) |
| |
| def update_from_raw_log(self, log_file): |
| action_map = self.action_map |
| for line in log_file: |
| try: |
| data = json.loads(line) |
| except ValueError: |
| # Just skip lines that aren't json |
| continue |
| action = data["action"] |
| if action in action_map: |
| action_map[action](data) |
| |
| def update_from_wptreport_log(self, data): |
| action_map = self.action_map |
| action_map["suite_start"]({"run_info": data["run_info"]}) |
| for test in data["results"]: |
| action_map["test_start"]({"test": test["test"]}) |
| for subtest in test["subtests"]: |
| action_map["test_status"]({"test": test["test"], |
| "subtest": subtest["name"], |
| "status": subtest["status"], |
| "expected": subtest.get("expected")}) |
| action_map["test_end"]({"test": test["test"], |
| "status": test["status"], |
| "expected": test.get("expected")}) |
| if "asserts" in test: |
| asserts = test["asserts"] |
| action_map["assertion_count"]({"test": test["test"], |
| "count": asserts["count"], |
| "min_expected": asserts["min"], |
| "max_expected": asserts["max"]}) |
| for item in data.get("lsan_leaks", []): |
| action_map["lsan_leak"](item) |
| |
| mozleak_data = data.get("mozleak", {}) |
| for scope, scope_data in mozleak_data.iteritems(): |
| for key, action in [("objects", "mozleak_object"), |
| ("total", "mozleak_total")]: |
| for item in scope_data.get(key, []): |
| item_data = {"scope": scope} |
| item_data.update(item) |
| action_map[action](item_data) |
| |
| def suite_start(self, data): |
| self.run_info = run_info_intern.store(data["run_info"]) |
| |
| def test_start(self, data): |
| test_id = intern(data["test"].encode("utf8")) |
| try: |
| test_data = self.id_test_map[test_id] |
| except KeyError: |
| print("Test not found %s, skipping" % test_id) |
| return |
| |
| if self.ignore_existing: |
| test_data.set_requires_update() |
| test_data.clear.add("expected") |
| self.tests_visited[test_id] = set() |
| |
| def test_status(self, data): |
| test_id = intern(data["test"].encode("utf8")) |
| subtest = intern(data["subtest"].encode("utf8")) |
| test_data = self.id_test_map.get(test_id) |
| if test_data is None: |
| return |
| |
| self.tests_visited[test_id].add(subtest) |
| |
| result = status_intern.store(data["status"]) |
| |
| test_data.set(test_id, subtest, "status", self.run_info, result) |
| if data.get("expected") and data["expected"] != data["status"]: |
| test_data.set_requires_update() |
| |
| def test_end(self, data): |
| if data["status"] == "SKIP": |
| return |
| |
| test_id = intern(data["test"].encode("utf8")) |
| test_data = self.id_test_map.get(test_id) |
| if test_data is None: |
| return |
| |
| result = status_intern.store(data["status"]) |
| |
| test_data.set(test_id, None, "status", self.run_info, result) |
| if data.get("expected") and data["status"] != data["expected"]: |
| test_data.set_requires_update() |
| del self.tests_visited[test_id] |
| |
| def assertion_count(self, data): |
| test_id = intern(data["test"].encode("utf8")) |
| test_data = self.id_test_map.get(test_id) |
| if test_data is None: |
| return |
| |
| test_data.set(test_id, None, "asserts", self.run_info, data["count"]) |
| if data["count"] < data["min_expected"] or data["count"] > data["max_expected"]: |
| test_data.set_requires_update() |
| |
| def test_for_scope(self, data): |
| dir_path = data.get("scope", "/") |
| dir_id = intern(os.path.join(dir_path, "__dir__").replace(os.path.sep, "/").encode("utf8")) |
| if dir_id.startswith("/"): |
| dir_id = dir_id[1:] |
| return dir_id, self.id_test_map[dir_id] |
| |
| def lsan_leak(self, data): |
| dir_id, test_data = self.test_for_scope(data) |
| test_data.set(dir_id, None, "lsan", |
| self.run_info, (data["frames"], data.get("allowed_match"))) |
| if not data.get("allowed_match"): |
| test_data.set_requires_update() |
| |
| def mozleak_object(self, data): |
| dir_id, test_data = self.test_for_scope(data) |
| test_data.set(dir_id, None, "leak-object", |
| self.run_info, ("%s:%s", (data["process"], data["name"]), |
| data.get("allowed"))) |
| if not data.get("allowed"): |
| test_data.set_requires_update() |
| |
| def mozleak_total(self, data): |
| if data["bytes"]: |
| dir_id, test_data = self.test_for_scope(data) |
| test_data.set(dir_id, None, "leak-threshold", |
| self.run_info, (data["process"], data["bytes"], data["threshold"])) |
| if data["bytes"] > data["threshold"] or data["bytes"] < 0: |
| test_data.set_requires_update() |
| |
| |
| def create_test_tree(metadata_path, test_manifest): |
| """Create a map of test_id to TestFileData for that test. |
| """ |
| id_test_map = {} |
| exclude_types = frozenset(["stub", "helper", "manual", "support", "conformancechecker"]) |
| all_types = manifestitem.item_types.keys() |
| include_types = set(all_types) - exclude_types |
| for item_type, test_path, tests in test_manifest.itertypes(*include_types): |
| test_file_data = TestFileData(intern(test_manifest.url_base.encode("utf8")), |
| intern(item_type.encode("utf8")), |
| metadata_path, |
| test_path, |
| tests) |
| for test in tests: |
| id_test_map[intern(test.id.encode("utf8"))] = test_file_data |
| |
| dir_path = os.path.split(test_path)[0].replace(os.path.sep, "/") |
| while True: |
| if dir_path: |
| dir_id = dir_path + "/__dir__" |
| else: |
| dir_id = "__dir__" |
| dir_id = intern((test_manifest.url_base + dir_id).lstrip("/").encode("utf8")) |
| if dir_id not in id_test_map: |
| test_file_data = TestFileData(intern(test_manifest.url_base.encode("utf8")), |
| None, |
| metadata_path, |
| dir_id, |
| []) |
| id_test_map[dir_id] = test_file_data |
| if not dir_path or dir_path in id_test_map: |
| break |
| dir_path = dir_path.rsplit("/", 1)[0] if "/" in dir_path else "" |
| |
| return id_test_map |
| |
| |
| class PackedResultList(object): |
| """Class for storing test results. |
| |
| Results are stored as an array of 2-byte integers for compactness. |
| The first 4 bits represent the property name, the second 4 bits |
| represent the test status (if it's a result with a status code), and |
| the final 8 bits represent the run_info. If the result doesn't have a |
| simple status code but instead a richer type, we place that richer type |
| in a dictionary and set the status part of the result type to 0. |
| |
| This class depends on the global prop_intern, run_info_intern and |
| status_intern InteredData objects to convert between the bit values |
| and corresponding Python objects.""" |
| |
| def __init__(self): |
| self.data = array.array("H") |
| |
| __slots__ = ("data", "raw_data") |
| |
| def append(self, prop, run_info, value): |
| out_val = (prop << 12) + run_info |
| if prop == prop_intern.store("status"): |
| out_val += value << 8 |
| else: |
| if not hasattr(self, "raw_data"): |
| self.raw_data = {} |
| self.raw_data[len(self.data)] = value |
| self.data.append(out_val) |
| |
| def unpack(self, idx, packed): |
| prop = prop_intern.get((packed & 0xF000) >> 12) |
| |
| value_idx = (packed & 0x0F00) >> 8 |
| if value_idx == 0: |
| value = self.raw_data[idx] |
| else: |
| value = status_intern.get(value_idx) |
| |
| run_info = run_info_intern.get((packed & 0x00FF)) |
| |
| return prop, run_info, value |
| |
| def __iter__(self): |
| for i, item in enumerate(self.data): |
| yield self.unpack(i, item) |
| |
| |
| class TestFileData(object): |
| __slots__ = ("url_base", "item_type", "test_path", "metadata_path", "tests", |
| "_requires_update", "clear", "data") |
| |
| def __init__(self, url_base, item_type, metadata_path, test_path, tests): |
| self.url_base = url_base |
| self.item_type = item_type |
| self.test_path = test_path |
| self.metadata_path = metadata_path |
| self.tests = {intern(item.id.encode("utf8")) for item in tests} |
| self._requires_update = False |
| self.clear = set() |
| self.data = defaultdict(lambda: defaultdict(PackedResultList)) |
| |
| def set_requires_update(self): |
| self._requires_update = True |
| |
| def set(self, test_id, subtest_id, prop, run_info, value): |
| self.data[test_id][subtest_id].append(prop_intern.store(prop), |
| run_info, |
| value) |
| |
| def expected(self, property_order, boolean_properties): |
| expected_data = load_expected(self.url_base, |
| self.metadata_path, |
| self.test_path, |
| self.tests, |
| property_order, |
| boolean_properties) |
| if expected_data is None: |
| expected_data = create_expected(self.url_base, |
| self.test_path, |
| property_order, |
| boolean_properties) |
| return expected_data |
| |
| def update(self, property_order, boolean_properties, stability, |
| default_expected_by_type): |
| if not self._requires_update: |
| return |
| |
| expected = self.expected(property_order, boolean_properties) |
| expected_by_test = {} |
| |
| for test_id in self.tests: |
| if not expected.has_test(test_id): |
| expected.append(manifestupdate.TestNode.create(test_id)) |
| test_expected = expected.get_test(test_id) |
| expected_by_test[test_id] = test_expected |
| for prop in self.clear: |
| test_expected.clear(prop) |
| |
| for test_id, test_data in self.data.iteritems(): |
| for subtest_id, results_list in test_data.iteritems(): |
| for prop, run_info, value in results_list: |
| # Special case directory metadata |
| if subtest_id is None and test_id.endswith("__dir__"): |
| if prop == "lsan": |
| expected.set_lsan(run_info, value) |
| elif prop == "leak-object": |
| expected.set_leak_object(run_info, value) |
| elif prop == "leak-threshold": |
| expected.set_leak_threshold(run_info, value) |
| continue |
| |
| if prop == "status": |
| value = Result(value, default_expected_by_type[self.item_type, |
| subtest_id is not None]) |
| |
| test_expected = expected_by_test[test_id] |
| if subtest_id is None: |
| item_expected = test_expected |
| else: |
| item_expected = test_expected.get_subtest(subtest_id) |
| if prop == "status": |
| item_expected.set_result(run_info, value) |
| elif prop == "asserts": |
| item_expected.set_asserts(run_info, value) |
| |
| expected.coalesce_properties(stability=stability) |
| for test in expected.iterchildren(): |
| for subtest in test.iterchildren(): |
| subtest.coalesce_properties(stability=stability) |
| test.coalesce_properties(stability=stability) |
| |
| return expected |
| |
| |
| Result = namedtuple("Result", ["status", "default_expected"]) |
| |
| |
| def create_expected(url_base, test_path, property_order=None, |
| boolean_properties=None): |
| expected = manifestupdate.ExpectedManifest(None, |
| test_path, |
| url_base, |
| property_order=property_order, |
| boolean_properties=boolean_properties) |
| return expected |
| |
| |
| def load_expected(url_base, metadata_path, test_path, tests, property_order=None, |
| boolean_properties=None): |
| expected_manifest = manifestupdate.get_manifest(metadata_path, |
| test_path, |
| url_base, |
| property_order=property_order, |
| boolean_properties=boolean_properties) |
| if expected_manifest is None: |
| return |
| |
| # Remove expected data for tests that no longer exist |
| for test in expected_manifest.iterchildren(): |
| if test.id not in tests: |
| test.remove() |
| |
| return expected_manifest |