Add support for the intermittent dashboard

Use the new intermittent dashboard to report intermittents and get
information about open bugs. This is now used to filter out
known-intermittents from results. In addition, this also allows the
scripts to report bug information to the GitHub. Display that in all
output.
This commit is contained in:
Martin Robinson 2023-02-14 14:32:44 +01:00
parent 0b98f33555
commit 2784c0e69d
3 changed files with 125 additions and 60 deletions

View file

@ -41,6 +41,13 @@ class Item:
title = f"{actual} [expected {expected}] {title_prefix}{title}" title = f"{actual} [expected {expected}] {title_prefix}{title}"
else: else:
title = f"{actual} {title_prefix}{title}" 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 "" stack = result["stack"] if result["stack"] and print_stack else ""
body = f"{result['message']}\n{stack}".strip() body = f"{result['message']}\n{stack}".strip()

View file

@ -41,24 +41,21 @@ class UnexpectedResult():
stack: Optional[str] stack: Optional[str]
unexpected_subtest_results: list[UnexpectedSubtestResult] = field( unexpected_subtest_results: list[UnexpectedSubtestResult] = field(
default_factory=list) default_factory=list)
issues: list[str] = field(default_factory=list)
def __str__(self): def __str__(self):
output = "" output = UnexpectedResult.to_lines(self)
if self.expected != self.actual:
lines = UnexpectedResult.to_lines(self)
output += UnexpectedResult.wrap_and_indent_lines(lines, " ")
if self.unexpected_subtest_results: 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 # Test names sometimes contain control characters, which we want
# to be printed in their raw form, and not their interpreted form. # to be printed in their raw form, and not their interpreted form.
path = result.path.encode('unicode-escape') lines = []
lines = [f"Unexpected subtest result in {path}:"]
for subtest in subtest_results[:-1]: for subtest in subtest_results[:-1]:
lines += UnexpectedResult.to_lines( lines += UnexpectedResult.to_lines(
subtest, print_stack=False) subtest, print_stack=False)
lines += UnexpectedResult.to_lines(subtest_results[-1]) 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 # 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 # 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. # Print stackless results first. They are all separate.
if None in results_by_stack: if None in results_by_stack:
output = make_subtests_failure( output += make_subtests_failure(results_by_stack.pop(None))
self, results_by_stack.pop(None))
for subtest_results in results_by_stack.values(): for subtest_results in results_by_stack.values():
output += make_subtests_failure(self, subtest_results) output += make_subtests_failure(subtest_results)
return output
return UnexpectedResult.wrap_and_indent_lines(output, " ")
@staticmethod @staticmethod
def wrap_and_indent_lines(lines, indent): def wrap_and_indent_lines(lines, indent):
@ -89,15 +86,18 @@ class UnexpectedResult():
@staticmethod @staticmethod
def to_lines(result: Any[UnexpectedSubtestResult, UnexpectedResult], print_stack=True): def to_lines(result: Any[UnexpectedSubtestResult, UnexpectedResult], print_stack=True):
first_line = result.actual
if result.expected != result.actual: if result.expected != result.actual:
expected_text = f" [expected {result.expected}]" first_line += f" [expected {result.expected}]"
else:
expected_text = u""
# Test names sometimes contain control characters, which we want # Test names sometimes contain control characters, which we want
# to be printed in their raw form, and not their interpreted form. # to be printed in their raw form, and not their interpreted form.
path = result.path.encode('unicode-escape') first_line += f" {result.path.encode('unicode-escape').decode('utf-8')}"
lines = [f"{result.actual}{expected_text} {path}"]
if isinstance(result, UnexpectedResult) and result.issues:
first_line += f" ({', '.join([f'#{bug}' for bug in result.issues])})"
lines = [first_line]
if result.message: if result.message:
for message_line in result.message.splitlines(): for message_line in result.message.splitlines():
lines.append(f" \u2192 {message_line}") lines.append(f" \u2192 {message_line}")

View file

@ -6,7 +6,9 @@ import dataclasses
import grouping_formatter import grouping_formatter
import json import json
import os import os
import re
import sys import sys
import urllib.error
import urllib.parse import urllib.parse
import urllib.request import urllib.request
@ -14,8 +16,8 @@ import mozlog
import mozlog.formatters import mozlog.formatters
import multiprocessing import multiprocessing
from typing import List from typing import List, NamedTuple, Optional, Tuple, Union
from grouping_formatter import UnexpectedResult from grouping_formatter import UnexpectedResult, UnexpectedSubtestResult
SCRIPT_PATH = os.path.abspath(os.path.dirname(__file__)) SCRIPT_PATH = os.path.abspath(os.path.dirname(__file__))
SERVO_ROOT = os.path.abspath(os.path.join(SCRIPT_PATH, "..", "..")) 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 = "https://build.servo.org/intermittent-tracker"
TRACKER_API_ENV_VAR = "INTERMITTENT_TRACKER_API" 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): 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 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): def __init__(self):
self.url = os.environ.get(TRACKER_API_ENV_VAR, TRACKER_API) base_url = os.environ.get(TRACKER_API_ENV_VAR, TRACKER_API)
if self.url.endswith("/"): self.headers = {
self.url = self.url[0:-1] "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): @staticmethod
query = urllib.parse.quote(test_name, safe='') def get_github_context_information() -> GithubContextInformation:
request = urllib.request.Request("%s/query.py?name=%s" % (self.url, query)) github_context = json.loads(os.environ.get("GITHUB_CONTEXT", "{}"))
search = urllib.request.urlopen(request) if not github_context:
return len(json.load(search)) > 0 return GithubContextInformation(None, None, None)
repository = github_context['repository']
repo_url = f"https://github.com/{repository}"
class GitHubQueryFilter(): run_id = github_context['run_id']
def __init__(self, token): build_url = f"{repo_url}/actions/runs/{run_id})"
self.token = token
def is_failure_intermittent(self, test_name): commit_title = github_context["event"]["head_commit"]["message"]
url = "https://api.github.com/search/issues?q=" match = re.match(r"^Auto merge of #(\d+)", commit_title)
query = "repo:servo/servo+" + \ pr_url = f"{repo_url}/pull/{match.group(1)}" if match else None
"label:I-intermittent+" + \
"type:issue+" + \
"state:open+" + \
test_name
# we want `/` to get quoted, but not `+` (github's API doesn't like return GithubContextInformation(
# that), so we set `safe` to `+` build_url,
url += urllib.parse.quote(query, safe="+") pr_url,
github_context["ref_name"]
)
request = urllib.request.Request(url) def make_data_from_result(
request.add_header("Authorization", f"Bearer: {self.token}") self,
request.add_header("Accept", "application/vnd.github+json") result: Union[UnexpectedResult, UnexpectedSubtestResult],
return json.load( ) -> dict:
urllib.request.urlopen(request) data = {
)["total_count"] > 0 '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( def filter_intermittents(
@ -230,19 +297,10 @@ def filter_intermittents(
print(80 * "=") print(80 * "=")
print(f"Filtering {len(unexpected_results)} unexpected " print(f"Filtering {len(unexpected_results)} unexpected "
"results for known intermittents") "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] = [] filter = TrackerDashboardFilter()
unexpected: List[UnexpectedResult] = [] (known_intermittents, unexpected) = \
for i, result in enumerate(unexpected_results): filter.filter_unexpected_results(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)
output = "\n".join([ output = "\n".join([
f"{len(known_intermittents)} known-intermittent unexpected result", f"{len(known_intermittents)} known-intermittent unexpected result",