summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/harness/wptrunner/manifestupdate.py
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/harness/wptrunner/manifestupdate.py')
-rw-r--r--testing/web-platform/harness/wptrunner/manifestupdate.py464
1 files changed, 464 insertions, 0 deletions
diff --git a/testing/web-platform/harness/wptrunner/manifestupdate.py b/testing/web-platform/harness/wptrunner/manifestupdate.py
new file mode 100644
index 000000000..0d992b9d5
--- /dev/null
+++ b/testing/web-platform/harness/wptrunner/manifestupdate.py
@@ -0,0 +1,464 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+import urlparse
+from collections import namedtuple, defaultdict
+
+from wptmanifest.node import (DataNode, ConditionalNode, BinaryExpressionNode,
+ BinaryOperatorNode, VariableNode, StringNode, NumberNode,
+ UnaryExpressionNode, UnaryOperatorNode, KeyValueNode)
+from wptmanifest.backends import conditional
+from wptmanifest.backends.conditional import ManifestItem
+
+import expected
+
+"""Manifest structure used to update the expected results of a test
+
+Each manifest file is represented by an ExpectedManifest that has one
+or more TestNode children, one per test in the manifest. Each
+TestNode has zero or more SubtestNode children, one for each known
+subtest of the test.
+
+In these representations, conditionals expressions in the manifest are
+not evaluated upfront but stored as python functions to be evaluated
+at runtime.
+
+When a result for a test is to be updated set_result on the
+[Sub]TestNode is called to store the new result, alongside the
+existing conditional that result's run info matched, if any. Once all
+new results are known, coalesce_expected is called to compute the new
+set of results and conditionals. The AST of the underlying parsed manifest
+is updated with the changes, and the result is serialised to a file.
+"""
+
+class ConditionError(Exception):
+ pass
+
+Result = namedtuple("Result", ["run_info", "status"])
+
+
+def data_cls_getter(output_node, visited_node):
+ # visited_node is intentionally unused
+ if output_node is None:
+ return ExpectedManifest
+ elif isinstance(output_node, ExpectedManifest):
+ return TestNode
+ elif isinstance(output_node, TestNode):
+ return SubtestNode
+ else:
+ raise ValueError
+
+
+class ExpectedManifest(ManifestItem):
+ def __init__(self, node, test_path=None, url_base=None, property_order=None,
+ boolean_properties=None):
+ """Object representing all the tests in a particular manifest
+
+ :param node: AST Node associated with this object. If this is None,
+ a new AST is created to associate with this manifest.
+ :param test_path: Path of the test file associated with this manifest.
+ :param url_base: Base url for serving the tests in this manifest.
+ :param property_order: List of properties to use in expectation metadata
+ from most to least significant.
+ :param boolean_properties: Set of properties in property_order that should
+ be treated as boolean.
+ """
+ if node is None:
+ node = DataNode(None)
+ ManifestItem.__init__(self, node)
+ self.child_map = {}
+ self.test_path = test_path
+ self.url_base = url_base
+ assert self.url_base is not None
+ self.modified = False
+ self.boolean_properties = boolean_properties
+ self.property_order = property_order
+
+ def append(self, child):
+ ManifestItem.append(self, child)
+ if child.id in self.child_map:
+ print "Warning: Duplicate heading %s" % child.id
+ self.child_map[child.id] = child
+
+ def _remove_child(self, child):
+ del self.child_map[child.id]
+ ManifestItem._remove_child(self, child)
+
+ def get_test(self, test_id):
+ """Return a TestNode by test id, or None if no test matches
+
+ :param test_id: The id of the test to look up"""
+
+ return self.child_map[test_id]
+
+ def has_test(self, test_id):
+ """Boolean indicating whether the current test has a known child test
+ with id test id
+
+ :param test_id: The id of the test to look up"""
+
+ return test_id in self.child_map
+
+ @property
+ def url(self):
+ return urlparse.urljoin(self.url_base,
+ "/".join(self.test_path.split(os.path.sep)))
+
+class TestNode(ManifestItem):
+ def __init__(self, node):
+ """Tree node associated with a particular test in a manifest
+
+ :param node: AST node associated with the test"""
+
+ ManifestItem.__init__(self, node)
+ self.updated_expected = []
+ self.new_expected = []
+ self.subtests = {}
+ self.default_status = None
+ self._from_file = True
+
+ @classmethod
+ def create(cls, test_type, test_id):
+ """Create a TestNode corresponding to a given test
+
+ :param test_type: The type of the test
+ :param test_id: The id of the test"""
+
+ url = test_id
+ name = url.split("/")[-1]
+ node = DataNode(name)
+ self = cls(node)
+
+ self.set("type", test_type)
+ self._from_file = False
+ return self
+
+ @property
+ def is_empty(self):
+ required_keys = set(["type"])
+ if set(self._data.keys()) != required_keys:
+ return False
+ return all(child.is_empty for child in self.children)
+
+ @property
+ def test_type(self):
+ """The type of the test represented by this TestNode"""
+
+ return self.get("type", None)
+
+ @property
+ def id(self):
+ """The id of the test represented by this TestNode"""
+ return urlparse.urljoin(self.parent.url, self.name)
+
+ def disabled(self, run_info):
+ """Boolean indicating whether this test is disabled when run in an
+ environment with the given run_info
+
+ :param run_info: Dictionary of run_info parameters"""
+
+ return self.get("disabled", run_info) is not None
+
+ def set_result(self, run_info, result):
+ """Set the result of the test in a particular run
+
+ :param run_info: Dictionary of run_info parameters corresponding
+ to this run
+ :param result: Status of the test in this run"""
+
+ if self.default_status is not None:
+ assert self.default_status == result.default_expected
+ else:
+ self.default_status = result.default_expected
+
+ # Add this result to the list of results satisfying
+ # any condition in the list of updated results it matches
+ for (cond, values) in self.updated_expected:
+ if cond(run_info):
+ values.append(Result(run_info, result.status))
+ if result.status != cond.value:
+ self.root.modified = True
+ break
+ else:
+ # We didn't find a previous value for this
+ self.new_expected.append(Result(run_info, result.status))
+ self.root.modified = True
+
+ def coalesce_expected(self):
+ """Update the underlying manifest AST for this test based on all the
+ added results.
+
+ This will update existing conditionals if they got the same result in
+ all matching runs in the updated results, will delete existing conditionals
+ that get more than one different result in the updated run, and add new
+ conditionals for anything that doesn't match an existing conditional.
+
+ Conditionals not matched by any added result are not changed."""
+
+ final_conditionals = []
+
+ try:
+ unconditional_status = self.get("expected")
+ except KeyError:
+ unconditional_status = self.default_status
+
+ for conditional_value, results in self.updated_expected:
+ if not results:
+ # The conditional didn't match anything in these runs so leave it alone
+ final_conditionals.append(conditional_value)
+ elif all(results[0].status == result.status for result in results):
+ # All the new values for this conditional matched, so update the node
+ result = results[0]
+ if (result.status == unconditional_status and
+ conditional_value.condition_node is not None):
+ if "expected" in self:
+ self.remove_value("expected", conditional_value)
+ else:
+ conditional_value.value = result.status
+ final_conditionals.append(conditional_value)
+ elif conditional_value.condition_node is not None:
+ # Blow away the existing condition and rebuild from scratch
+ # This isn't sure to work if we have a conditional later that matches
+ # these values too, but we can hope, verify that we get the results
+ # we expect, and if not let a human sort it out
+ self.remove_value("expected", conditional_value)
+ self.new_expected.extend(results)
+ elif conditional_value.condition_node is None:
+ self.new_expected.extend(result for result in results
+ if result.status != unconditional_status)
+
+ # It is an invariant that nothing in new_expected matches an existing
+ # condition except for the default condition
+
+ if self.new_expected:
+ if all(self.new_expected[0].status == result.status
+ for result in self.new_expected) and not self.updated_expected:
+ status = self.new_expected[0].status
+ if status != self.default_status:
+ self.set("expected", status, condition=None)
+ final_conditionals.append(self._data["expected"][-1])
+ else:
+ try:
+ conditionals = group_conditionals(
+ self.new_expected,
+ property_order=self.root.property_order,
+ boolean_properties=self.root.boolean_properties)
+ except ConditionError:
+ print "Conflicting test results for %s, cannot update" % self.root.test_path
+ return
+ for conditional_node, status in conditionals:
+ if status != unconditional_status:
+ self.set("expected", status, condition=conditional_node.children[0])
+ final_conditionals.append(self._data["expected"][-1])
+
+ if ("expected" in self._data and
+ len(self._data["expected"]) > 0 and
+ self._data["expected"][-1].condition_node is None and
+ self._data["expected"][-1].value == self.default_status):
+
+ self.remove_value("expected", self._data["expected"][-1])
+
+ if ("expected" in self._data and
+ len(self._data["expected"]) == 0):
+ for child in self.node.children:
+ if (isinstance(child, KeyValueNode) and
+ child.data == "expected"):
+ child.remove()
+ break
+
+ def _add_key_value(self, node, values):
+ ManifestItem._add_key_value(self, node, values)
+ if node.data == "expected":
+ self.updated_expected = []
+ for value in values:
+ self.updated_expected.append((value, []))
+
+ def clear_expected(self):
+ """Clear all the expected data for this test and all of its subtests"""
+
+ self.updated_expected = []
+ if "expected" in self._data:
+ for child in self.node.children:
+ if (isinstance(child, KeyValueNode) and
+ child.data == "expected"):
+ child.remove()
+ del self._data["expected"]
+ break
+
+ for subtest in self.subtests.itervalues():
+ subtest.clear_expected()
+
+ def append(self, node):
+ child = ManifestItem.append(self, node)
+ self.subtests[child.name] = child
+
+ def get_subtest(self, name):
+ """Return a SubtestNode corresponding to a particular subtest of
+ the current test, creating a new one if no subtest with that name
+ already exists.
+
+ :param name: Name of the subtest"""
+
+ if name in self.subtests:
+ return self.subtests[name]
+ else:
+ subtest = SubtestNode.create(name)
+ self.append(subtest)
+ return subtest
+
+
+class SubtestNode(TestNode):
+ def __init__(self, node):
+ assert isinstance(node, DataNode)
+ TestNode.__init__(self, node)
+
+ @classmethod
+ def create(cls, name):
+ node = DataNode(name)
+ self = cls(node)
+ return self
+
+ @property
+ def is_empty(self):
+ if self._data:
+ return False
+ return True
+
+
+def group_conditionals(values, property_order=None, boolean_properties=None):
+ """Given a list of Result objects, return a list of
+ (conditional_node, status) pairs representing the conditional
+ expressions that are required to match each status
+
+ :param values: List of Results
+ :param property_order: List of properties to use in expectation metadata
+ from most to least significant.
+ :param boolean_properties: Set of properties in property_order that should
+ be treated as boolean."""
+
+ by_property = defaultdict(set)
+ for run_info, status in values:
+ for prop_name, prop_value in run_info.iteritems():
+ by_property[(prop_name, prop_value)].add(status)
+
+ if property_order is None:
+ property_order = ["debug", "os", "version", "processor", "bits"]
+
+ if boolean_properties is None:
+ boolean_properties = set(["debug"])
+ else:
+ boolean_properties = set(boolean_properties)
+
+ # If we have more than one value, remove any properties that are common
+ # for all the values
+ if len(values) > 1:
+ for key, statuses in by_property.copy().iteritems():
+ if len(statuses) == len(values):
+ del by_property[key]
+ if not by_property:
+ raise ConditionError
+
+ properties = set(item[0] for item in by_property.iterkeys())
+ include_props = []
+
+ for prop in property_order:
+ if prop in properties:
+ include_props.append(prop)
+
+ conditions = {}
+
+ for run_info, status in values:
+ prop_set = tuple((prop, run_info[prop]) for prop in include_props)
+ if prop_set in conditions:
+ continue
+
+ expr = make_expr(prop_set, status, boolean_properties=boolean_properties)
+ conditions[prop_set] = (expr, status)
+
+ return conditions.values()
+
+
+def make_expr(prop_set, status, boolean_properties=None):
+ """Create an AST that returns the value ``status`` given all the
+ properties in prop_set match.
+
+ :param prop_set: tuple of (property name, value) pairs for each
+ property in this expression and the value it must match
+ :param status: Status on RHS when all the given properties match
+ :param boolean_properties: Set of properties in property_order that should
+ be treated as boolean.
+ """
+ root = ConditionalNode()
+
+ assert len(prop_set) > 0
+
+ expressions = []
+ for prop, value in prop_set:
+ number_types = (int, float, long)
+ value_cls = (NumberNode
+ if type(value) in number_types
+ else StringNode)
+ if prop not in boolean_properties:
+ expressions.append(
+ BinaryExpressionNode(
+ BinaryOperatorNode("=="),
+ VariableNode(prop),
+ value_cls(unicode(value))
+ ))
+ else:
+ if value:
+ expressions.append(VariableNode(prop))
+ else:
+ expressions.append(
+ UnaryExpressionNode(
+ UnaryOperatorNode("not"),
+ VariableNode(prop)
+ ))
+ if len(expressions) > 1:
+ prev = expressions[-1]
+ for curr in reversed(expressions[:-1]):
+ node = BinaryExpressionNode(
+ BinaryOperatorNode("and"),
+ curr,
+ prev)
+ prev = node
+ else:
+ node = expressions[0]
+
+ root.append(node)
+ root.append(StringNode(status))
+
+ return root
+
+
+def get_manifest(metadata_root, test_path, url_base, property_order=None,
+ boolean_properties=None):
+ """Get the ExpectedManifest for a particular test path, or None if there is no
+ metadata stored for that test path.
+
+ :param metadata_root: Absolute path to the root of the metadata directory
+ :param test_path: Path to the test(s) relative to the test root
+ :param url_base: Base url for serving the tests in this manifest
+ :param property_order: List of properties to use in expectation metadata
+ from most to least significant.
+ :param boolean_properties: Set of properties in property_order that should
+ be treated as boolean."""
+ manifest_path = expected.expected_path(metadata_root, test_path)
+ try:
+ with open(manifest_path) as f:
+ return compile(f, test_path, url_base, property_order=property_order,
+ boolean_properties=boolean_properties)
+ except IOError:
+ return None
+
+
+def compile(manifest_file, test_path, url_base, property_order=None,
+ boolean_properties=None):
+ return conditional.compile(manifest_file,
+ data_cls_getter=data_cls_getter,
+ test_path=test_path,
+ url_base=url_base,
+ property_order=property_order,
+ boolean_properties=boolean_properties)