1 # This Source Code Form is subject to the terms of the Mozilla Public
2 # License, v. 2.0. If a copy of the MPL was not distributed with this
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
10 from collections
import defaultdict
11 from urllib
import parse
as urlparse
14 from wptrunner
import expected
15 from wptrunner
.wptmanifest
.backends
import base
16 from wptrunner
.wptmanifest
.serializer
import serialize
18 here
= os
.path
.dirname(__file__
)
19 logger
= logging
.getLogger(__name__
)
23 class Compiler(base
.Compiler
):
24 def visit_KeyValueNode(self
, node
):
27 for child
in node
.children
:
28 values
.append(self
.visit(child
))
30 self
.output_node
.set(key_name
, values
)
32 def visit_ConditionalNode(self
, node
):
33 assert len(node
.children
) == 2
34 # For conditional nodes, just return the subtree
35 return node
.children
[0], self
.visit(node
.children
[1])
37 def visit_UnaryExpressionNode(self
, node
):
38 raise NotImplementedError
40 def visit_BinaryExpressionNode(self
, node
):
41 raise NotImplementedError
43 def visit_UnaryOperatorNode(self
, node
):
44 raise NotImplementedError
46 def visit_BinaryOperatorNode(self
, node
):
47 raise NotImplementedError
50 class ExpectedManifest(base
.ManifestItem
):
51 def __init__(self
, node
, test_path
, url_base
):
52 """Object representing all the tests in a particular manifest
54 :param name: Name of the AST Node associated with this object.
55 Should always be None since this should always be associated with
56 the root node of the AST.
57 :param test_path: Path of the test file associated with this manifest.
58 :param url_base: Base url for serving the tests in this manifest
61 raise ValueError("ExpectedManifest requires a test path")
63 raise ValueError("ExpectedManifest requires a base url")
64 base
.ManifestItem
.__init
__(self
, node
)
66 self
.test_path
= test_path
67 self
.url_base
= url_base
69 def append(self
, child
):
70 """Add a test to the manifest"""
71 base
.ManifestItem
.append(self
, child
)
72 self
.child_map
[child
.id] = child
76 return urlparse
.urljoin(
77 self
.url_base
, "/".join(self
.test_path
.split(os
.path
.sep
))
81 class DirectoryManifest(base
.ManifestItem
):
85 class TestManifestItem(base
.ManifestItem
):
86 def __init__(self
, node
, **kwargs
):
87 """Tree node associated with a particular test in a manifest
89 :param name: name of the test"""
90 base
.ManifestItem
.__init
__(self
, node
)
95 return urlparse
.urljoin(self
.parent
.url
, self
.name
)
97 def append(self
, node
):
98 """Add a subtest to the current test
100 :param node: AST Node associated with the subtest"""
101 child
= base
.ManifestItem
.append(self
, node
)
102 self
.subtests
[child
.name
] = child
104 def get_subtest(self
, name
):
105 """Get the SubtestNode corresponding to a particular subtest, by name
107 :param name: Name of the node to return"""
108 if name
in self
.subtests
:
109 return self
.subtests
[name
]
113 class SubtestManifestItem(TestManifestItem
):
117 def data_cls_getter(output_node
, visited_node
):
118 # visited_node is intentionally unused
119 if output_node
is None:
120 return ExpectedManifest
121 if isinstance(output_node
, ExpectedManifest
):
122 return TestManifestItem
123 if isinstance(output_node
, TestManifestItem
):
124 return SubtestManifestItem
128 def get_manifest(metadata_root
, test_path
, url_base
):
129 """Get the ExpectedManifest for a particular test path, or None if there is no
130 metadata stored for that test path.
132 :param metadata_root: Absolute path to the root of the metadata directory
133 :param test_path: Path to the test(s) relative to the test root
134 :param url_base: Base url for serving the tests in this manifest
135 :param run_info: Dictionary of properties of the test run for which the expectation
136 values should be computed.
138 manifest_path
= expected
.expected_path(metadata_root
, test_path
)
140 with
open(manifest_path
, "rb") as f
:
143 data_cls_getter
=data_cls_getter
,
151 def get_dir_manifest(path
):
152 """Get the ExpectedManifest for a particular test path, or None if there is no
153 metadata stored for that test path.
155 :param path: Full path to the ini file
156 :param run_info: Dictionary of properties of the test run for which the expectation
157 values should be computed.
160 with
open(path
, "rb") as f
:
161 return compile(f
, data_cls_getter
=lambda x
, y
: DirectoryManifest
)
166 def compile(stream
, data_cls_getter
=None, **kwargs
):
167 return base
.compile(Compiler
, stream
, data_cls_getter
=data_cls_getter
, **kwargs
)
171 parser
= argparse
.ArgumentParser()
172 parser
.add_argument("--out-dir", help="Directory to store output files")
174 "--meta-dir", help="Directory containing wpt-metadata " "checkout to update."
179 def run(src_root
, obj_root
, logger_
=None, **kwargs
):
180 logger_obj
= logger_
if logger_
is not None else logger
182 manifests
= manifestupdate
.run(src_root
, obj_root
, logger_obj
, **kwargs
)
187 for meta_root
, test_path
, test_metadata
in iter_tests(manifests
):
188 for dir_path
in get_dir_paths(meta_root
, test_path
):
189 if dir_path
not in dirs_seen
:
190 dirs_seen
.add(dir_path
)
191 dir_manifest
= get_dir_manifest(dir_path
)
192 rel_path
= os
.path
.relpath(dir_path
, meta_root
)
194 add_manifest(rv
, rel_path
, dir_manifest
)
197 add_manifest(rv
, test_path
, test_metadata
)
199 if kwargs
["out_dir"]:
200 if not os
.path
.exists(kwargs
["out_dir"]):
201 os
.makedirs(kwargs
["out_dir"])
202 out_path
= os
.path
.join(kwargs
["out_dir"], "summary.json")
203 with
open(out_path
, "w") as f
:
206 print(json
.dumps(rv
, indent
=2))
208 if kwargs
["meta_dir"]:
209 update_wpt_meta(logger_obj
, kwargs
["meta_dir"], rv
)
212 def get_dir_paths(test_root
, test_path
):
213 if not os
.path
.isabs(test_path
):
214 test_path
= os
.path
.join(test_root
, test_path
)
215 dir_path
= os
.path
.dirname(test_path
)
216 while dir_path
!= test_root
:
217 yield os
.path
.join(dir_path
, "__dir__.ini")
218 dir_path
= os
.path
.dirname(dir_path
)
219 assert len(dir_path
) >= len(test_root
)
222 def iter_tests(manifests
):
223 for manifest
in manifests
.keys():
224 for test_type
, test_path
, tests
in manifest
:
225 url_base
= manifests
[manifest
]["url_base"]
226 metadata_base
= manifests
[manifest
]["metadata_path"]
227 expected_manifest
= get_manifest(metadata_base
, test_path
, url_base
)
228 if expected_manifest
:
229 yield metadata_base
, test_path
, expected_manifest
232 def add_manifest(target
, path
, metadata
):
233 dir_name
, file_name
= path
.rsplit(os
.sep
, 1)
236 add_metadata(target
, key
, metadata
)
240 for test_metadata
in metadata
.children
:
241 key
.append(test_metadata
.name
)
242 add_metadata(target
, key
, test_metadata
)
243 add_filename(target
, key
, file_name
)
244 key
.append("_subtests")
245 for subtest_metadata
in test_metadata
.children
:
246 key
.append(subtest_metadata
.name
)
247 add_metadata(target
, key
, subtest_metadata
)
261 statuses
= set(["CRASH"])
264 def add_filename(target
, key
, filename
):
266 if part
not in target
:
268 target
= target
[part
]
270 target
["_filename"] = filename
273 def add_metadata(target
, key
, metadata
):
274 if not is_interesting(metadata
):
278 if part
not in target
:
280 target
= target
[part
]
282 for prop
in simple_props
:
283 if metadata
.has_key(prop
): # noqa W601
284 target
[prop
] = get_condition_value_list(metadata
, prop
)
286 if metadata
.has_key("expected"): # noqa W601
288 values
= metadata
.get("expected")
289 by_status
= defaultdict(list)
291 if isinstance(item
, tuple):
292 condition
, status
= item
296 if isinstance(status
, list):
297 intermittent
.append((condition
, status
))
298 expected_status
= status
[0]
300 expected_status
= status
301 by_status
[expected_status
].append(condition
)
302 for status
in statuses
:
303 if status
in by_status
:
304 target
["expected_%s" % status
] = [
305 serialize(item
) if item
else None for item
in by_status
[status
]
308 target
["intermittent"] = [
309 [serialize(cond
) if cond
else None, intermittent_statuses
]
310 for cond
, intermittent_statuses
in intermittent
314 def get_condition_value_list(metadata
, key
):
316 for item
in metadata
.get(key
):
317 if isinstance(item
, tuple):
318 assert len(item
) == 2
319 conditions
.append((serialize(item
[0]), item
[1]))
321 conditions
.append((None, item
))
325 def is_interesting(metadata
):
326 if any(metadata
.has_key(prop
) for prop
in simple_props
): # noqa W601
329 if metadata
.has_key("expected"): # noqa W601
330 for expected_value
in metadata
.get("expected"):
331 # Include both expected and known intermittent values
332 if isinstance(expected_value
, tuple):
333 expected_value
= expected_value
[1]
334 if isinstance(expected_value
, list):
336 if expected_value
in statuses
:
342 def update_wpt_meta(logger
, meta_root
, data
):
346 if not os
.path
.exists(meta_root
) or not os
.path
.isdir(meta_root
):
347 raise ValueError("%s is not a directory" % (meta_root
,))
349 with
WptMetaCollection(meta_root
) as wpt_meta
:
350 for dir_path
, dir_data
in sorted(data
.items()):
351 for test
, test_data
in dir_data
.get("_tests", {}).items():
352 add_test_data(logger
, wpt_meta
, dir_path
, test
, None, test_data
)
353 for subtest
, subtest_data
in test_data
.get("_subtests", {}).items():
355 logger
, wpt_meta
, dir_path
, test
, subtest
, subtest_data
359 def add_test_data(logger
, wpt_meta
, dir_path
, test
, subtest
, test_data
):
360 triage_keys
= ["bug"]
362 for key
in triage_keys
:
364 value
= test_data
[key
]
365 for cond_value
in value
:
366 if cond_value
[0] is not None:
367 logger
.info("Skipping conditional metadata")
369 cond_value
= cond_value
[1]
370 if not isinstance(cond_value
, list):
371 cond_value
= [cond_value
]
372 for bug_value
in cond_value
:
373 bug_link
= get_bug_link(bug_value
)
375 logger
.info("Could not extract bug: %s" % value
)
377 meta
= wpt_meta
.get(dir_path
)
378 meta
.set(test
, subtest
, product
="firefox", bug_url
=bug_link
)
381 bugzilla_re
= re
.compile("https://bugzilla\.mozilla\.org/show_bug\.cgi\?id=\d+")
382 bug_re
= re
.compile("(?:[Bb][Uu][Gg])?\s*(\d+)")
385 def get_bug_link(value
):
386 value
= value
.strip()
387 m
= bugzilla_re
.match(value
)
390 m
= bug_re
.match(value
)
392 return "https://bugzilla.mozilla.org/show_bug.cgi?id=%s" % m
.group(1)
395 class WptMetaCollection(object):
396 def __init__(self
, root
):
403 def __exit__(self
, *args
, **kwargs
):
404 for item
in self
.loaded
.itervalues():
405 item
.write(self
.root
)
408 def get(self
, dir_path
):
409 if dir_path
not in self
.loaded
:
410 meta
= WptMeta
.get_or_create(self
.root
, dir_path
)
411 self
.loaded
[dir_path
] = meta
412 return self
.loaded
[dir_path
]
415 class WptMeta(object):
416 def __init__(self
, dir_path
, data
):
417 assert "links" in data
and isinstance(data
["links"], list)
418 self
.dir_path
= dir_path
422 def meta_path(meta_root
, dir_path
):
423 return os
.path
.join(meta_root
, dir_path
, "META.yml")
425 def path(self
, meta_root
):
426 return self
.meta_path(meta_root
, self
.dir_path
)
429 def get_or_create(cls
, meta_root
, dir_path
):
430 if os
.path
.exists(cls
.meta_path(meta_root
, dir_path
)):
431 return cls
.load(meta_root
, dir_path
)
432 return cls(dir_path
, {"links": []})
435 def load(cls
, meta_root
, dir_path
):
436 with
open(cls
.meta_path(meta_root
, dir_path
), "r") as f
:
437 data
= yaml
.safe_load(f
)
438 return cls(dir_path
, data
)
440 def set(self
, test
, subtest
, product
, bug_url
):
442 for link
in self
.data
["links"]:
443 link_product
= link
.get("product")
445 link_product
= link_product
.split("-", 1)[0]
446 if link_product
is None or link_product
== product
:
447 if link
["url"] == bug_url
:
451 if target_link
is None:
453 "product": product
.encode("utf8"),
454 "url": bug_url
.encode("utf8"),
457 self
.data
["links"].append(target_link
)
459 if "results" not in target_link
:
460 target_link
["results"] = []
463 (result
["test"] == test
and result
.get("subtest") == subtest
)
464 for result
in target_link
["results"]
467 data
= {"test": test
.encode("utf8")}
469 data
["subtest"] = subtest
.encode("utf8")
470 target_link
["results"].append(data
)
472 def write(self
, meta_root
):
473 path
= self
.path(meta_root
)
474 dirname
= os
.path
.dirname(path
)
475 if not os
.path
.exists(dirname
):
477 with
open(path
, "wb") as f
:
478 yaml
.safe_dump(self
.data
, f
, default_flow_style
=False, allow_unicode
=True)