diff options
author | bors-servo <infra@servo.org> | 2023-02-16 02:44:57 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-02-16 02:44:57 +0100 |
commit | 9bcc55fc9a3ae5102d034caaad369851f787f271 (patch) | |
tree | fbf269792098eecd3d1b621da910ddf437a84a1f | |
parent | aaa2348a25814dca389171f5df9d16e1cbbe7fab (diff) | |
parent | 2784c0e69d5e960444444a14a67ef2fbf3073f35 (diff) | |
download | servo-9bcc55fc9a3ae5102d034caaad369851f787f271.tar.gz servo-9bcc55fc9a3ae5102d034caaad369851f787f271.zip |
Auto merge of #29359 - mrobinson:intermittent-dashboard, r=delan
Add support for the intermittent dashboard
<!-- Please describe your changes on the following line: -->
---
<!-- Thank you for contributing to Servo! Please replace each `[ ]` by `[X]` when the step is complete, and replace `___` with appropriate data: -->
- [x] `./mach build -d` does not report any errors
- [x] `./mach test-tidy` does not report any errors
- [x] These changes do not require tests because this is a CI change.
<!-- Also, please make sure that "Allow edits from maintainers" checkbox is checked, so that we can help you if you get stuck somewhere along the way.-->
<!-- Pull requests that do not address these steps are welcome, but they will require additional verification as part of the review process. -->
-rwxr-xr-x | etc/ci/report_aggregated_expected_results.py | 7 | ||||
-rw-r--r-- | tests/wpt/grouping_formatter.py | 34 | ||||
-rw-r--r-- | tests/wpt/servowpt.py | 146 |
3 files changed, 126 insertions, 61 deletions
diff --git a/etc/ci/report_aggregated_expected_results.py b/etc/ci/report_aggregated_expected_results.py index d67a2abe34f..b9d9a0650cb 100755 --- a/etc/ci/report_aggregated_expected_results.py +++ b/etc/ci/report_aggregated_expected_results.py @@ -41,6 +41,13 @@ class Item: title = f"{actual} [expected {expected}] {title_prefix}{title}" else: title = f"{actual} {title_prefix}{title}" + + issue_url = "http://github.com/servo/servo/issues/" + if "issues" in result and result["issues"]: + issues = ", ".join([f"[#{issue}]({issue_url}{issue})" + for issue in result["issues"]]) + title += f" ({issues})" + stack = result["stack"] if result["stack"] and print_stack else "" body = f"{result['message']}\n{stack}".strip() diff --git a/tests/wpt/grouping_formatter.py b/tests/wpt/grouping_formatter.py index bad9395ecbc..36151572c10 100644 --- a/tests/wpt/grouping_formatter.py +++ b/tests/wpt/grouping_formatter.py @@ -41,24 +41,21 @@ class UnexpectedResult(): stack: Optional[str] unexpected_subtest_results: list[UnexpectedSubtestResult] = field( default_factory=list) + issues: list[str] = field(default_factory=list) def __str__(self): - output = "" - if self.expected != self.actual: - lines = UnexpectedResult.to_lines(self) - output += UnexpectedResult.wrap_and_indent_lines(lines, " ") + output = UnexpectedResult.to_lines(self) if self.unexpected_subtest_results: - def make_subtests_failure(result, subtest_results): + def make_subtests_failure(subtest_results): # Test names sometimes contain control characters, which we want # to be printed in their raw form, and not their interpreted form. - path = result.path.encode('unicode-escape') - lines = [f"Unexpected subtest result in {path}:"] + lines = [] for subtest in subtest_results[:-1]: lines += UnexpectedResult.to_lines( subtest, print_stack=False) lines += UnexpectedResult.to_lines(subtest_results[-1]) - return self.wrap_and_indent_lines(lines, " ") + return self.wrap_and_indent_lines(lines, " ").splitlines() # Organize the failures by stack trace so we don't print the same stack trace # more than once. They are really tall and we don't want to flood the screen @@ -69,11 +66,11 @@ class UnexpectedResult(): # Print stackless results first. They are all separate. if None in results_by_stack: - output = make_subtests_failure( - self, results_by_stack.pop(None)) + output += make_subtests_failure(results_by_stack.pop(None)) for subtest_results in results_by_stack.values(): - output += make_subtests_failure(self, subtest_results) - return output + output += make_subtests_failure(subtest_results) + + return UnexpectedResult.wrap_and_indent_lines(output, " ") @staticmethod def wrap_and_indent_lines(lines, indent): @@ -89,15 +86,18 @@ class UnexpectedResult(): @staticmethod def to_lines(result: Any[UnexpectedSubtestResult, UnexpectedResult], print_stack=True): + first_line = result.actual if result.expected != result.actual: - expected_text = f" [expected {result.expected}]" - else: - expected_text = u"" + first_line += f" [expected {result.expected}]" # Test names sometimes contain control characters, which we want # to be printed in their raw form, and not their interpreted form. - path = result.path.encode('unicode-escape') - lines = [f"{result.actual}{expected_text} {path}"] + first_line += f" {result.path.encode('unicode-escape').decode('utf-8')}" + + if isinstance(result, UnexpectedResult) and result.issues: + first_line += f" ({', '.join([f'#{bug}' for bug in result.issues])})" + + lines = [first_line] if result.message: for message_line in result.message.splitlines(): lines.append(f" \u2192 {message_line}") diff --git a/tests/wpt/servowpt.py b/tests/wpt/servowpt.py index 241ee86a52a..72cfdf7c03b 100644 --- a/tests/wpt/servowpt.py +++ b/tests/wpt/servowpt.py @@ -6,7 +6,9 @@ import dataclasses import grouping_formatter import json import os +import re import sys +import urllib.error import urllib.parse import urllib.request @@ -14,8 +16,8 @@ import mozlog import mozlog.formatters import multiprocessing -from typing import List -from grouping_formatter import UnexpectedResult +from typing import List, NamedTuple, Optional, Tuple, Union +from grouping_formatter import UnexpectedResult, UnexpectedSubtestResult SCRIPT_PATH = os.path.abspath(os.path.dirname(__file__)) SERVO_ROOT = os.path.abspath(os.path.join(SCRIPT_PATH, "..", "..")) @@ -28,7 +30,7 @@ import update # noqa: F401,E402 TRACKER_API = "https://build.servo.org/intermittent-tracker" TRACKER_API_ENV_VAR = "INTERMITTENT_TRACKER_API" -GITHUB_API_TOKEN_ENV_VAR = "INTERMITTENT_TRACKER_GITHUB_API_TOKEN" +TRACKER_DASHBOARD_SECRET_ENV_VAR = "INTERMITTENT_TRACKER_DASHBOARD_SECRET" def determine_build_type(kwargs: dict, target_dir: str): @@ -186,41 +188,106 @@ def update_tests(**kwargs): return 1 if return_value is update.exit_unclean else 0 -class TrackerFilter(): +class GithubContextInformation(NamedTuple): + build_url: Optional[str] + pull_url: Optional[str] + branch_name: Optional[str] + + +class TrackerDashboardFilter(): def __init__(self): - self.url = os.environ.get(TRACKER_API_ENV_VAR, TRACKER_API) - if self.url.endswith("/"): - self.url = self.url[0:-1] + base_url = os.environ.get(TRACKER_API_ENV_VAR, TRACKER_API) + self.headers = { + "Content-Type": "application/json" + } + if TRACKER_DASHBOARD_SECRET_ENV_VAR in os.environ: + self.url = f"{base_url}/dashboard/attempts" + secret = os.environ[TRACKER_DASHBOARD_SECRET_ENV_VAR] + self.headers["Authorization"] = f"Bearer {secret}" + else: + self.url = f"{base_url}/dashboard/query" - def is_failure_intermittent(self, test_name): - query = urllib.parse.quote(test_name, safe='') - request = urllib.request.Request("%s/query.py?name=%s" % (self.url, query)) - search = urllib.request.urlopen(request) - return len(json.load(search)) > 0 + @staticmethod + def get_github_context_information() -> GithubContextInformation: + github_context = json.loads(os.environ.get("GITHUB_CONTEXT", "{}")) + if not github_context: + return GithubContextInformation(None, None, None) + repository = github_context['repository'] + repo_url = f"https://github.com/{repository}" -class GitHubQueryFilter(): - def __init__(self, token): - self.token = token + run_id = github_context['run_id'] + build_url = f"{repo_url}/actions/runs/{run_id})" - def is_failure_intermittent(self, test_name): - url = "https://api.github.com/search/issues?q=" - query = "repo:servo/servo+" + \ - "label:I-intermittent+" + \ - "type:issue+" + \ - "state:open+" + \ - test_name + commit_title = github_context["event"]["head_commit"]["message"] + match = re.match(r"^Auto merge of #(\d+)", commit_title) + pr_url = f"{repo_url}/pull/{match.group(1)}" if match else None - # we want `/` to get quoted, but not `+` (github's API doesn't like - # that), so we set `safe` to `+` - url += urllib.parse.quote(query, safe="+") + return GithubContextInformation( + build_url, + pr_url, + github_context["ref_name"] + ) - request = urllib.request.Request(url) - request.add_header("Authorization", f"Bearer: {self.token}") - request.add_header("Accept", "application/vnd.github+json") - return json.load( - urllib.request.urlopen(request) - )["total_count"] > 0 + def make_data_from_result( + self, + result: Union[UnexpectedResult, UnexpectedSubtestResult], + ) -> dict: + data = { + 'path': result.path, + 'subtest': None, + 'expected': result.expected, + 'actual': result.actual, + 'time': result.time // 1000, + 'message': result.message, + 'stack': result.stack, + } + if isinstance(result, UnexpectedSubtestResult): + data["subtest"] = result.subtest + return data + + def filter_unexpected_results( + self, + unexpected_results: List[UnexpectedResult] + ) -> Tuple[List[UnexpectedResult], List[UnexpectedResult]]: + attempts = [] + for result in unexpected_results: + attempts.append(self.make_data_from_result(result)) + for subtest_result in result.unexpected_subtest_results: + attempts.append(self.make_data_from_result(subtest_result)) + + context = self.get_github_context_information() + try: + request = urllib.request.Request( + url=self.url, + method='POST', + data=json.dumps({ + 'branch': context.branch_name, + 'build_url': context.build_url, + 'pull_url': context.pull_url, + 'attempts': attempts + }).encode('utf-8'), + headers=self.headers) + + known_intermittents = dict() + with urllib.request.urlopen(request) as response: + for test in json.load(response)["known"]: + known_intermittents[test["path"]] = \ + [issue["number"] for issue in test["issues"]] + + except urllib.error.HTTPError as e: + print(e) + print(e.readlines()) + raise(e) + + known = [result for result in unexpected_results + if result.path in known_intermittents] + unknown = [result for result in unexpected_results + if result.path not in known_intermittents] + for result in known: + result.issues = known_intermittents[result.path] + + return (known, unknown) def filter_intermittents( @@ -230,19 +297,10 @@ def filter_intermittents( print(80 * "=") print(f"Filtering {len(unexpected_results)} unexpected " "results for known intermittents") - if GITHUB_API_TOKEN_ENV_VAR in os.environ: - filter = GitHubQueryFilter(os.environ.get(GITHUB_API_TOKEN_ENV_VAR)) - else: - filter = TrackerFilter() - - known_intermittents: List[UnexpectedResult] = [] - unexpected: List[UnexpectedResult] = [] - for i, result in enumerate(unexpected_results): - print(f" [{i}/{len(unexpected_results)}]", file=sys.stderr, end="\r") - if filter.is_failure_intermittent(result.path): - known_intermittents.append(result) - else: - unexpected.append(result) + + filter = TrackerDashboardFilter() + (known_intermittents, unexpected) = \ + filter.filter_unexpected_results(unexpected_results) output = "\n".join([ f"{len(known_intermittents)} known-intermittent unexpected result", |