• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mozilla / relman-auto-nag / #5419

12 Feb 2025 09:11PM CUT coverage: 21.189% (-0.007%) from 21.196%
#5419

push

coveralls-python

benjaminmah
Replaced nested double quotes

426 of 2954 branches covered (14.42%)

0 of 1 new or added line in 1 file covered. (0.0%)

40 existing lines in 1 file now uncovered.

1943 of 9170 relevant lines covered (21.19%)

0.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/bugbot/rules/component.py
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 file,
3
# You can obtain one at http://mozilla.org/MPL/2.0/.
4

5

6
from libmozdata.bugzilla import Bugzilla
×
7

8
from bugbot import logger
×
9
from bugbot.bugbug_utils import get_bug_ids_classification
×
10
from bugbot.bzcleaner import BzCleaner
×
11
from bugbot.utils import get_config, nice_round
×
12

13

14
class Component(BzCleaner):
×
15
    def __init__(self):
×
16
        super().__init__()
×
17
        self.autofix_component = {}
×
18
        self.frequency = "daily"
×
19
        self.general_confidence_threshold = self.get_config(
×
20
            "general_confidence_threshold"
21
        )
22
        self.component_confidence_threshold = self.get_config("confidence_threshold")
×
23
        self.fenix_confidence_threshold = self.get_config("fenix_confidence_threshold")
×
24

25
    def add_custom_arguments(self, parser):
×
26
        parser.add_argument(
×
27
            "--frequency",
28
            help="Daily (noisy) or Hourly",
29
            choices=["daily", "hourly"],
30
            default="daily",
31
        )
32

33
    def parse_custom_arguments(self, args):
×
34
        self.frequency = args.frequency
×
35

36
    def description(self):
×
37
        return f"[Using ML] Assign a component to untriaged bugs ({self.frequency})"
×
38

39
    def columns(self):
×
40
        return ["id", "summary", "component", "confidence", "autofixed"]
×
41

42
    def sort_columns(self):
×
43
        return lambda p: (-p[3], -int(p[0]))
×
44

45
    def has_product_component(self):
×
46
        # Inject product and components when calling BzCleaner.get_bugs
47
        return True
×
48

49
    def get_bz_params(self, date):
×
50
        start_date, end_date = self.get_dates(date)
×
51

52
        bot = get_config("common", "bot_bz_mail")[0]
×
53

54
        return {
×
55
            "include_fields": ["id", "groups", "summary", "product", "component"],
56
            # Ignore bugs for which we ever modified the product or the component.
57
            "n1": 1,
58
            "f1": "product",
59
            "o1": "changedby",
60
            "v1": bot,
61
            "n2": 1,
62
            "f2": "component",
63
            "o2": "changedby",
64
            "v2": bot,
65
            # Ignore closed bugs.
66
            "bug_status": "__open__",
67
            # Get recent General bugs, and all Untriaged bugs.
68
            "j3": "OR",
69
            "f3": "OP",
70
            "j4": "AND",
71
            "f4": "OP",
72
            "f5": "component",
73
            "o5": "equals",
74
            "v5": "General",
75
            "f6": "creation_ts",
76
            "o6": "greaterthan",
77
            "v6": start_date,
78
            "f7": "CP",
79
            "f8": "component",
80
            "o8": "anyexact",
81
            "v8": "Untriaged,Foxfooding",
82
            "f9": "CP",
83
        }
84

85
    def get_bugs(self, date="today", bug_ids=[]):
×
86
        def meets_threshold(bug_data):
×
87
            threshold = (
×
88
                self.general_confidence_threshold
89
                if bug_data["class"] == "Fenix" or bug_data["class"] == "General"
90
                else self.component_confidence_threshold
91
            )
92
            return bug_data["prob"][bug_data["index"]] >= threshold
×
93

94
        # Retrieve the bugs with the fields defined in get_bz_params
95
        raw_bugs = super().get_bugs(date=date, bug_ids=bug_ids, chunk_size=7000)
×
96

97
        if len(raw_bugs) == 0:
×
98
            return {}
×
99

100
        # Extract the bug ids
101
        bug_ids = list(raw_bugs.keys())
×
102

103
        # Classify those bugs
104
        bugs = get_bug_ids_classification("component", bug_ids)
×
105

106
        fenix_general_bug_ids = []
×
107
        for bug_id, bug_data in bugs.items():
×
108
            if not bug_data.get("available", True):
×
109
                # The bug was not available, it was either removed or is a
110
                # security bug.
111
                continue
×
112
            if meets_threshold(bug_data):
×
113
                if bug_data.get("class") == "Fenix":
×
114
                    fenix_general_bug_ids.append(bug_id)
×
115
            else:
116
                current_bug_data = raw_bugs[bug_id]
×
117
                if (
×
118
                    current_bug_data["product"] == "Fenix"
119
                    and current_bug_data["component"] == "General"
120
                ):
121
                    fenix_general_bug_ids.append(bug_id)
×
122

123
        if fenix_general_bug_ids:
×
124
            fenix_general_classification = get_bug_ids_classification(
×
125
                "fenixcomponent", fenix_general_bug_ids
126
            )
127

128
            for bug_id, data in fenix_general_classification.items():
×
129
                confidence = data["prob"][data["index"]]
×
130

131
                if confidence > self.fenix_confidence_threshold:
×
132
                    print(f"classification: {data['class']}")
×
133
                    if data["class"] == "General":
×
UNCOV
134
                        data["class"] = "GeckoView::General"
×
135
                    else:
UNCOV
136
                        data["class"] = f"Fenix::{data['class']}"
×
137
                    bugs[bug_id] = data
×
138

UNCOV
139
        results = {}
×
140

UNCOV
141
        for bug_id in sorted(bugs.keys()):
×
UNCOV
142
            bug_data = bugs[bug_id]
×
143

UNCOV
144
            if not bug_data.get("available", True):
×
145
                # The bug was not available, it was either removed or is a
146
                # security bug
UNCOV
147
                continue
×
148

149
            if not {"prob", "index", "class", "extra_data"}.issubset(bug_data.keys()):
×
150
                raise Exception(f"Invalid bug response {bug_id}: {bug_data!r}")
×
151

UNCOV
152
            bug = raw_bugs[bug_id]
×
153
            prob = bug_data["prob"]
×
UNCOV
154
            index = bug_data["index"]
×
UNCOV
155
            suggestion = bug_data["class"]
×
156

UNCOV
157
            conflated_components_mapping = bug_data["extra_data"].get(
×
158
                "conflated_components_mapping", {}
159
            )
160

161
            # Skip product-only suggestions that are not useful.
162
            if "::" not in suggestion and bug["product"] == suggestion:
×
UNCOV
163
                continue
×
164

165
            # No need to move a bug to the same component.
NEW
166
            if f"{bug['product']}::{bug['component']}" == suggestion:
×
167
                continue
×
168

169
            suggestion = conflated_components_mapping.get(suggestion, suggestion)
×
170

UNCOV
171
            if "::" not in suggestion:
×
UNCOV
172
                logger.error(
×
173
                    f"There is something wrong with this component suggestion! {suggestion}"
174
                )
175
                continue
×
176

177
            i = suggestion.index("::")
×
UNCOV
178
            suggested_product = suggestion[:i]
×
UNCOV
179
            suggested_component = suggestion[i + 2 :]
×
180

181
            # When moving bugs out of the 'General' component, we don't want to change the product (unless it is Firefox).
UNCOV
182
            if bug["component"] == "General" and bug["product"] not in {
×
183
                suggested_product,
184
                "Firefox",
185
            }:
UNCOV
186
                continue
×
187

188
            # Don't move bugs from Firefox::General to Core::Internationalization.
UNCOV
189
            if (
×
190
                bug["product"] == "Firefox"
191
                and bug["component"] == "General"
192
                and suggested_product == "Core"
193
                and suggested_component == "Internationalization"
194
            ):
195
                continue
×
196

UNCOV
197
            result = {
×
198
                "id": bug_id,
199
                "summary": bug["summary"],
200
                "component": suggestion,
201
                "confidence": nice_round(prob[index]),
202
                "autofixed": False,
203
            }
204

205
            # In daily mode, we send an email with all results.
UNCOV
206
            if self.frequency == "daily":
×
207
                results[bug_id] = result
×
208

UNCOV
209
            confidence_threshold_conf = (
×
210
                "confidence_threshold"
211
                if bug["component"] != "General"
212
                else "general_confidence_threshold"
213
            )
214

UNCOV
215
            if prob[index] >= self.get_config(confidence_threshold_conf):
×
UNCOV
216
                self.autofix_component[bug_id] = {
×
217
                    "product": suggested_product,
218
                    "component": suggested_component,
219
                }
220

UNCOV
221
                result["autofixed"] = True
×
222

223
                # In hourly mode, we send an email with only the bugs we acted upon.
UNCOV
224
                if self.frequency == "hourly":
×
UNCOV
225
                    results[bug_id] = result
×
226

227
        # Don't move bugs back into components they were moved out of.
228
        # TODO: Use the component suggestion from the service with the second highest confidence instead.
UNCOV
229
        def history_handler(bug):
×
230
            bug_id = str(bug["id"])
×
231

232
            previous_product_components = set()
×
233

UNCOV
234
            current_product = raw_bugs[bug_id]["product"]
×
235
            current_component = raw_bugs[bug_id]["component"]
×
236

237
            for history in bug["history"]:
×
238
                for change in history["changes"][::-1]:
×
239
                    if change["field_name"] == "product":
×
240
                        current_product = change["removed"]
×
UNCOV
241
                    elif change["field_name"] == "component":
×
242
                        current_component = change["removed"]
×
243

244
                previous_product_components.add((current_product, current_component))
×
245

UNCOV
246
            suggested_product = self.autofix_component[bug_id]["product"]
×
247
            suggested_component = self.autofix_component[bug_id]["component"]
×
248

249
            if (suggested_product, suggested_component) in previous_product_components:
×
UNCOV
250
                results[bug_id]["autofixed"] = False
×
251
                del self.autofix_component[bug_id]
×
252

UNCOV
253
        bugids = list(self.autofix_component.keys())
×
UNCOV
254
        Bugzilla(
×
255
            bugids=bugids,
256
            historyhandler=history_handler,
257
        ).get_data().wait()
258

259
        return results
×
260

261
    def get_autofix_change(self):
×
UNCOV
262
        cc = self.get_config("cc")
×
UNCOV
263
        return {
×
264
            bug_id: (
265
                data.update(
266
                    {
267
                        "cc": {"add": cc},
268
                        "comment": {
269
                            "body": f"The [Bugbug](https://github.com/mozilla/bugbug/) bot thinks this bug should belong to the '{data['product']}::{data['component']}' component, and is moving the bug to that component. Please correct in case you think the bot is wrong."
270
                        },
271
                    }
272
                )
273
                or data
274
            )
275
            for bug_id, data in self.autofix_component.items()
276
        }
277

UNCOV
278
    def get_db_extra(self):
×
UNCOV
279
        return {
×
280
            bugid: "{}::{}".format(v["product"], v["component"])
281
            for bugid, v in self.get_autofix_change().items()
282
        }
283

284

UNCOV
285
if __name__ == "__main__":
×
UNCOV
286
    Component().run()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc