tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

intermittent_failures.py (6121B)


      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/.
      4 
      5 """
      6 Shared module for fetching intermittent test failure data from Treeherder and Bugzilla.
      7 """
      8 
      9 import datetime
     10 import re
     11 from typing import Literal, Optional, TypedDict
     12 
     13 import requests
     14 from wpt_path_utils import resolve_wpt_path
     15 
     16 USER_AGENT = "mach-intermittent-failures/1.0"
     17 
     18 
     19 class BugzillaFailure(TypedDict):
     20    bug_id: int
     21    bug_count: int
     22 
     23 
     24 class BugzillaSummary(TypedDict):
     25    summary: str
     26    id: int
     27    status: Optional[str]
     28    resolution: Optional[str]
     29    creation_time: Optional[str]
     30    last_change_time: Optional[str]
     31    comment_count: Optional[int]
     32 
     33 
     34 class IntermittentFailure(TypedDict):
     35    bug_id: int
     36    failure_count: int
     37    summary: str
     38    status: str
     39    resolution: str
     40    test_path: Optional[str]
     41    creation_time: Optional[str]
     42    last_change_time: Optional[str]
     43    comment_count: Optional[int]
     44 
     45 
     46 class IntermittentFailuresFetcher:
     47    """Fetches intermittent test failure data from Treeherder and Bugzilla APIs."""
     48 
     49    def __init__(self, days: int = 7, threshold: int = 30, verbose: bool = False):
     50        self.days = days
     51        self.threshold = threshold
     52        self.verbose = verbose
     53        self.end_date = datetime.datetime.now()
     54        self.start_date = self.end_date - datetime.timedelta(days=self.days)
     55 
     56    def get_failures(self, branch: str = "trunk") -> list[IntermittentFailure]:
     57        """
     58        Fetch intermittent failures that meet the threshold.
     59 
     60        Returns a list of intermittent failures with bug information.
     61        """
     62        bugzilla_failures = self._get_bugzilla_failures(branch)
     63        bug_list = self._keep_bugs_above_threshold(bugzilla_failures)
     64 
     65        if not bug_list:
     66            return []
     67 
     68        bug_summaries = self._get_bugzilla_summaries(bug_list)
     69 
     70        results = []
     71        for bug in bug_summaries:
     72            bug_id = bug["id"]
     73            if bug_id in bug_list:
     74                result: IntermittentFailure = {
     75                    "bug_id": bug_id,
     76                    "failure_count": self._get_failure_count(bugzilla_failures, bug_id),
     77                    "summary": bug["summary"],
     78                    "status": bug.get("status", "UNKNOWN"),
     79                    "resolution": bug.get("resolution", ""),
     80                    "test_path": None,
     81                    "creation_time": bug.get("creation_time"),
     82                    "last_change_time": bug.get("last_change_time"),
     83                    "comment_count": bug.get("comment_count"),
     84                }
     85 
     86                if "single tracking bug" in bug["summary"]:
     87                    match = re.findall(
     88                        r" ([^\s]+\/?\.[a-z0-9-A-Z]+) \|", bug["summary"]
     89                    )
     90                    if match:
     91                        test_path = match[0]
     92                        if test_path.startswith("/"):
     93                            test_path = resolve_wpt_path(test_path)
     94                        result["test_path"] = test_path
     95 
     96                results.append(result)
     97 
     98        return results
     99 
    100    def get_single_tracking_bugs_with_paths(
    101        self, branch: str = "trunk"
    102    ) -> list[tuple[int, str]]:
    103        """
    104        Get only single tracking bugs that have test paths.
    105        This is what high_freq_skipfails uses.
    106 
    107        Returns a list of (bug_id, test_path) tuples.
    108        """
    109        failures = self.get_failures(branch)
    110 
    111        results = []
    112        for failure in failures:
    113            if failure["test_path"] and "single tracking bug" in failure["summary"]:
    114                results.append((failure["bug_id"], failure["test_path"]))
    115 
    116        return results
    117 
    118    def _get_bugzilla_failures(self, branch: str = "trunk") -> list[BugzillaFailure]:
    119        """Fetch failure data from Treeherder API."""
    120        url = (
    121            f"https://treeherder.mozilla.org/api/failures/"
    122            f"?startday={self.start_date.date()}&endday={self.end_date.date()}"
    123            f"&tree={branch}&failurehash=all"
    124        )
    125        if self.verbose:
    126            print(f"[DEBUG] Fetching failures from Treeherder: {url}")
    127        response = requests.get(url, headers={"User-agent": USER_AGENT})
    128        response.raise_for_status()
    129 
    130        return [
    131            item
    132            for item in response.json()
    133            if "bug_id" in item and isinstance(item["bug_id"], int)
    134        ]
    135 
    136    def _keep_bugs_above_threshold(
    137        self, failure_list: list[BugzillaFailure]
    138    ) -> list[int]:
    139        """Filter bugs that have failure counts above the threshold."""
    140        if not failure_list:
    141            return []
    142 
    143        bug_counts = {}
    144        for failure in failure_list:
    145            bug_id = failure["bug_id"]
    146            bug_counts[bug_id] = bug_counts.get(bug_id, 0) + failure.get("bug_count", 1)
    147 
    148        return [
    149            bug_id for bug_id, count in bug_counts.items() if count >= self.threshold
    150        ]
    151 
    152    def _get_failure_count(
    153        self, failure_list: list[BugzillaFailure], bug_id: int
    154    ) -> int:
    155        """Get the total failure count for a specific bug."""
    156        total = 0
    157        for failure in failure_list:
    158            if failure["bug_id"] == bug_id:
    159                total += failure.get("bug_count", 1)
    160        return total
    161 
    162    def _get_bugzilla_summaries(self, bug_id_list: list[int]) -> list[BugzillaSummary]:
    163        """Fetch bug summaries from Bugzilla REST API."""
    164        if not bug_id_list:
    165            return []
    166 
    167        url = (
    168            f"https://bugzilla.mozilla.org/rest/bug"
    169            f"?include_fields=summary,id,status,resolution,creation_time,last_change_time,comment_count"
    170            f"&id={','.join(str(id) for id in bug_id_list)}"
    171        )
    172        if self.verbose:
    173            print(f"[DEBUG] Fetching bug details from Bugzilla: {url}")
    174        response = requests.get(url, headers={"User-agent": USER_AGENT})
    175        response.raise_for_status()
    176 
    177        json_response: dict[Literal["bugs"], list[BugzillaSummary]] = response.json()
    178        return json_response.get("bugs", [])