Bug 1354232 - Add support for updating LSAN data in wpt-update r=maja_zf

LSAN data differs from existing expectation data because the data is
only generated when the browser exits, so the problems reported can
happen at any point in the current session. We use the `scope`
property in the log message to determine the path to a __dir__.ini
file that covers all the tests run in the session, and add the LSAN
exclusion rules to there.

The rules themselves are generated by taking the topmost frame of any
stack that's reported as unexpectedly leaking, and adding that to the
list of permitted frames in the lsan-allowed property. We never remove
entries from this list since intermittents may be present which won't
appear on a specific run. Instead we rely on humans fixing the issues
to also clean up the expectation files.

MozReview-Commit-ID: Kxm0hFXlGE3
This commit is contained in:
James Graham 2018-05-29 18:18:40 +01:00
Родитель a68bcb9344
Коммит 795ce832d0
4 изменённых файлов: 137 добавлений и 24 удалений

Просмотреть файл

@ -180,24 +180,32 @@ class FirefoxBrowser(Browser):
self.init_timeout = self.init_timeout * timeout_multiplier self.init_timeout = self.init_timeout * timeout_multiplier
self.asan = asan self.asan = asan
self.lsan_allowed = None
self.leak_check = leak_check self.leak_check = leak_check
self.leak_report_file = None self.leak_report_file = None
self.lsan_handler = None self.lsan_handler = None
if self.asan:
self.lsan_handler = mozleak.LSANLeaks(logger)
self.stylo_threads = stylo_threads self.stylo_threads = stylo_threads
self.chaos_mode_flags = chaos_mode_flags self.chaos_mode_flags = chaos_mode_flags
def settings(self, test): def settings(self, test):
if self.asan: self.lsan_allowed = test.lsan_allowed
self.lsan_handler.set_allowed(test.lsan_allowed) return {"check_leaks": self.leak_check and not test.leaks,
return {"check_leaks": self.leak_check and not test.leaks} "lsan_allowed": test.lsan_allowed}
def start(self, group_metadata=None, **kwargs):
if group_metadata is None:
group_metadata = {}
def start(self, **kwargs):
if self.marionette_port is None: if self.marionette_port is None:
self.marionette_port = get_free_port(2828, exclude=self.used_ports) self.marionette_port = get_free_port(2828, exclude=self.used_ports)
self.used_ports.add(self.marionette_port) self.used_ports.add(self.marionette_port)
if self.asan:
print "Setting up LSAN"
self.lsan_handler = mozleak.LSANLeaks(self.logger,
scope=group_metadata.get("scope", "/"),
allowed=self.lsan_allowed)
env = test_environment(xrePath=os.path.dirname(self.binary), env = test_environment(xrePath=os.path.dirname(self.binary),
debugger=self.debug_info is not None, debugger=self.debug_info is not None,
log=self.logger, log=self.logger,

Просмотреть файл

@ -75,6 +75,9 @@ class ExpectedManifest(ManifestItem):
self.modified = False self.modified = False
self.boolean_properties = boolean_properties self.boolean_properties = boolean_properties
self.property_order = property_order self.property_order = property_order
self.update_properties = {
"lsan": LsanUpdate(self),
}
def append(self, child): def append(self, child):
ManifestItem.append(self, child) ManifestItem.append(self, child)
@ -106,6 +109,19 @@ class ExpectedManifest(ManifestItem):
return urlparse.urljoin(self.url_base, return urlparse.urljoin(self.url_base,
"/".join(self.test_path.split(os.path.sep))) "/".join(self.test_path.split(os.path.sep)))
def set_lsan(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: Lsan violations detected"""
self.update_properties["lsan"].set(run_info, result)
def coalesce_properties(self, stability):
for prop_update in self.update_properties.itervalues():
prop_update.coalesce(stability)
class TestNode(ManifestItem): class TestNode(ManifestItem):
def __init__(self, node): def __init__(self, node):
@ -475,6 +491,44 @@ class MinAssertsUpdate(PropertyUpdate):
return True, new_value return True, new_value
class LsanUpdate(PropertyUpdate):
property_name = "lsan-allowed"
cls_default_value = None
def get_value(self, result):
# IF we have an allowed_match that matched, return None
# This value is ignored later (because it matches the default)
# We do that because then if we allow a failure in foo/__dir__.ini
# we don't want to update foo/bar/__dir__.ini with the same rule
if result[1]:
return None
# Otherwise return the topmost stack frame
# TODO: there is probably some improvement to be made by looking for a "better" stack frame
return result[0][0]
def update_value(self, old_value, new_value):
if isinstance(new_value, (str, unicode)):
new_value = {new_value}
else:
new_value = set(new_value)
if old_value is None:
old_value = set()
old_value = set(old_value)
return sorted((old_value | new_value) - {None})
def update_default(self):
current_default = None
if self.property_name in self.node._data:
current_default = [item for item in
self.node._data[self.property_name]
if item.condition_node is None]
if current_default:
current_default = current_default[0].value
new_values = [item.value for item in self.new]
new_value = self.update_value(current_default, new_values)
return True, new_value if new_value else None
def group_conditionals(values, property_order=None, boolean_properties=None): def group_conditionals(values, property_order=None, boolean_properties=None):
"""Given a list of Value objects, return a list of """Given a list of Value objects, return a list of
(conditional_node, status) pairs representing the conditional (conditional_node, status) pairs representing the conditional

Просмотреть файл

@ -3,7 +3,6 @@ import shutil
import tempfile import tempfile
import uuid import uuid
from mozlog import reader
from mozlog import structuredlog from mozlog import structuredlog
import expected import expected
@ -18,11 +17,9 @@ manifestitem = None
logger = structuredlog.StructuredLogger("web-platform-tests") logger = structuredlog.StructuredLogger("web-platform-tests")
try: try:
import ujson import ujson as json
except ImportError: except ImportError:
pass import json
else:
reader.json = ujson
def load_test_manifests(serve_root, test_paths): def load_test_manifests(serve_root, test_paths):
@ -167,6 +164,7 @@ def update_from_logs(manifests, *log_filenames, **kwargs):
for manifest_expected in expected_map.itervalues(): for manifest_expected in expected_map.itervalues():
for tree in manifest_expected.itervalues(): for tree in manifest_expected.itervalues():
tree.coalesce_properties(stability=stability)
for test in tree.iterchildren(): for test in tree.iterchildren():
for subtest in test.iterchildren(): for subtest in test.iterchildren():
subtest.coalesce_properties(stability=stability) subtest.coalesce_properties(stability=stability)
@ -238,7 +236,8 @@ class ExpectedUpdater(object):
"test_start": self.test_start, "test_start": self.test_start,
"test_status": self.test_status, "test_status": self.test_status,
"test_end": self.test_end, "test_end": self.test_end,
"assertion_count": self.assertion_count} "assertion_count": self.assertion_count,
"lsan_leak": self.lsan_leak}
self.tests_visited = {} self.tests_visited = {}
self.test_cache = {} self.test_cache = {}
@ -251,8 +250,12 @@ class ExpectedUpdater(object):
def update_from_log(self, log_file): def update_from_log(self, log_file):
self.run_info = None self.run_info = None
log_reader = reader.read(log_file) action_map = self.action_map
reader.each_log(log_reader, self.action_map) for line in log_file:
data = json.loads(line)
action = data["action"]
if action in action_map:
action_map[action](data)
def suite_start(self, data): def suite_start(self, data):
self.run_info = data["run_info"] self.run_info = data["run_info"]
@ -314,6 +317,16 @@ class ExpectedUpdater(object):
test.set_asserts(self.run_info, data["count"]) test.set_asserts(self.run_info, data["count"])
def lsan_leak(self, data):
dir_path = data.get("scope", "/")
dir_id = os.path.join(dir_path, "__dir__").replace(os.path.sep, "/")
if dir_id.startswith("/"):
dir_id = dir_id[1:]
test_manifest, obj = self.id_path_map[dir_id]
expected_node = self.expected_tree[test_manifest][obj]
expected_node.set_lsan(self.run_info, (data["frames"], data.get("allowed_match")))
def create_test_tree(metadata_path, test_manifest, property_order=None, def create_test_tree(metadata_path, test_manifest, property_order=None,
boolean_properties=None): boolean_properties=None):
@ -326,23 +339,60 @@ def create_test_tree(metadata_path, test_manifest, property_order=None,
item.item_type is not None] item.item_type is not None]
include_types = set(all_types) - exclude_types include_types = set(all_types) - exclude_types
for _, test_path, tests in test_manifest.itertypes(*include_types): for _, test_path, tests in test_manifest.itertypes(*include_types):
expected_data = load_expected(test_manifest, metadata_path, test_path, tests, expected_data = load_or_create_expected(test_manifest, metadata_path, test_path, tests,
property_order=property_order, property_order, boolean_properties)
boolean_properties=boolean_properties)
if expected_data is None:
expected_data = create_expected(test_manifest,
test_path,
tests,
property_order=property_order,
boolean_properties=boolean_properties)
for test in tests: for test in tests:
id_test_map[test.id] = (test_manifest, test) id_test_map[test.id] = (test_manifest, test)
expected_map[test] = expected_data expected_map[test] = expected_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 = (test_manifest.url_base + dir_id).lstrip("/")
if dir_id not in id_test_map:
dir_object = DirObject(dir_id, dir_path)
expected_data = load_or_create_expected(test_manifest,
metadata_path,
dir_id,
[],
property_order,
boolean_properties)
id_test_map[dir_id] = (test_manifest, dir_object)
expected_map[dir_object] = expected_data
if not dir_path:
break
dir_path = dir_path.rsplit("/", 1)[0] if "/" in dir_path else ""
return expected_map, id_test_map return expected_map, id_test_map
class DirObject(object):
def __init__(self, id, path):
self.id = id
self.path = path
def __hash__(self):
return hash(self.id)
def load_or_create_expected(test_manifest, metadata_path, test_path, tests, property_order=None,
boolean_properties=None):
expected_data = load_expected(test_manifest, metadata_path, test_path, tests,
property_order=property_order,
boolean_properties=boolean_properties)
if expected_data is None:
expected_data = create_expected(test_manifest,
test_path,
tests,
property_order=property_order,
boolean_properties=boolean_properties)
return expected_data
def create_expected(test_manifest, test_path, tests, property_order=None, def create_expected(test_manifest, test_path, tests, property_order=None,
boolean_properties=None): boolean_properties=None):
expected = manifestupdate.ExpectedManifest(None, test_path, test_manifest.url_base, expected = manifestupdate.ExpectedManifest(None, test_path, test_manifest.url_base,

Просмотреть файл

@ -455,6 +455,7 @@ def iterfilter(filters, iter):
for item in iter: for item in iter:
yield item yield item
class TestLoader(object): class TestLoader(object):
def __init__(self, def __init__(self,
test_manifests, test_manifests,