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

mozilla / relman-auto-nag / #4649

pending completion
#4649

push

coveralls-python

suhaibmujahid
Create a standalone logic to determine version status flags

713 of 3543 branches covered (20.12%)

106 of 106 new or added lines in 2 files covered. (100.0%)

1917 of 8684 relevant lines covered (22.08%)

0.22 hits per line

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

69.38
/bugbot/rules/regression_set_status_flags.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
from itertools import chain
1✔
6

7
from libmozdata.bugzilla import Bugzilla
1✔
8

9
from bugbot import logger, utils
1✔
10
from bugbot.bug.analyzer import BugNotInStoreError, BugsStore
1✔
11
from bugbot.bzcleaner import BzCleaner
1✔
12

13

14
class RegressionSetStatusFlags(BzCleaner):
1✔
15
    def __init__(self):
1✔
16
        super().__init__()
1✔
17
        if not self.init_versions():
1!
18
            return
×
19

20
        self.status_esr = utils.get_flag(self.versions["esr_previous"], "status", "esr")
1✔
21
        self.status_esr_next = utils.get_flag(self.versions["esr"], "status", "esr")
1✔
22
        self.status_changes = {}
1✔
23
        self.component_exception = set(
1✔
24
            utils.get_config(self.name(), "component_exception")
25
        )
26

27
    def description(self):
1✔
28
        return "Set release status flags based on info from the regressing bug"
1✔
29

30
    def get_bz_params(self, date):
1✔
31
        start_date, _ = self.get_dates(date)
×
32

33
        # Find all bugs with regressed_by information which were open after start_date or
34
        # whose regressed_by field was set after start_date.
35

36
        return {
×
37
            "include_fields": ["regressed_by", "_custom", "product", "component"],
38
            "f1": "OP",
39
            "j1": "OR",
40
            "f2": "creation_ts",
41
            "o2": "greaterthan",
42
            "v2": start_date,
43
            "f3": "regressed_by",
44
            "o3": "changedafter",
45
            "v3": start_date,
46
            "f4": "CP",
47
            "f5": "regressed_by",
48
            "o5": "isnotempty",
49
            "f6": "cf_status_firefox_release",
50
            "o6": "nowords",
51
            "v6": "fixed,verified",
52
            "resolution": ["---", "FIXED"],
53
            "bug_status_type": "notequals",
54
            "bug_status": "UNCONFIRMED",
55
        }
56

57
    def get_extra_for_template(self):
1✔
58
        if self.status_esr == self.status_esr_next:
×
59
            return [f"esr{self.versions['esr']}"]
×
60
        return [f"esr{self.versions['esr_previous']}", f"esr{self.versions['esr']}"]
×
61

62
    def handle_bug(self, bug, data):
1✔
63
        pc = f"{bug['product']}::{bug['component']}"
×
64
        if pc in self.component_exception:
×
65
            return
×
66

67
        bugid = bug["id"]
×
68
        if len(bug["regressed_by"]) != 1:
×
69
            # either we don't have access to the regressor, or there's more than one, either way leave things alone
70
            return
×
71
        bug["regressed_by"] = bug["regressed_by"][0]
×
72
        data[str(bugid)] = bug
×
73

74
    def get_flags_from_regressing_bugs(self, bugids):
1✔
75
        def bug_handler(bug, data):
×
76
            data[bug["id"]] = bug
×
77

78
        data = {}
×
79
        Bugzilla(
×
80
            bugids=list(bugids), bughandler=bug_handler, bugdata=data
81
        ).get_data().wait()
82

83
        return data
×
84

85
    @staticmethod
1✔
86
    def _is_latest_status_flag_wontfix(bug: dict) -> bool:
1✔
87
        """Check if the latest status flag is wontfix or fix-optional."""
88
        versions_status = sorted(
1✔
89
            (int(flag[len("cf_status_firefox") :]), status)
90
            for flag, status in bug.items()
91
            if status != "---"
92
            and flag.startswith("cf_status_firefox")
93
            and "esr" not in flag
94
        )
95

96
        if not versions_status:
1✔
97
            return False
1✔
98

99
        status = versions_status[-1][1]
1✔
100

101
        return status in ("wontfix", "fix-optional")
1✔
102

103
    def get_status_changes(self, bugs):
1✔
104
        bugids = {info["regressed_by"] for info in bugs.values()}
1✔
105
        if not bugids:
1!
106
            return {}
×
107

108
        data = self.get_flags_from_regressing_bugs(bugids)
1✔
109

110
        filtered_bugs = {}
1✔
111
        for bugid, info in bugs.items():
1✔
112
            regressor = info["regressed_by"]
1✔
113
            if regressor not in data:
1!
114
                logger.error(
×
115
                    "Rule %s: regressor %s is not found",
116
                    self.name(),
117
                    regressor,
118
                )
119
                # This should be OK in local environment where we don't have
120
                # access to all bugs, but it is not OK in production.
121
                continue
×
122

123
            regression_versions = sorted(
1✔
124
                v
125
                for v in data[regressor]
126
                if v.startswith("cf_status_firefox")
127
                and data[regressor][v] in ("fixed", "verified")
128
            )
129
            if not regression_versions:
1!
130
                # don't know what to do, ignore
131
                continue
×
132
            if regression_versions[0].startswith("cf_status_firefox_esr"):
1!
133
                # shouldn't happen: esrXX sorts after YY
134
                continue
×
135
            regressed_version = int(regression_versions[0][len("cf_status_firefox") :])
1✔
136

137
            fixed_versions = sorted(
1✔
138
                v
139
                for v in info
140
                if v.startswith("cf_status_firefox")
141
                and info[v] in ("fixed", "verified")
142
            )
143
            if len(fixed_versions) > 0 and fixed_versions[0].startswith(
1!
144
                "cf_status_firefox_esr"
145
            ):
146
                # shouldn't happen: esrXX sorts after YY
147
                continue
×
148
            fixed_version = (
1✔
149
                int(fixed_versions[0][len("cf_status_firefox") :])
150
                if len(fixed_versions) > 0
151
                else None
152
            )
153

154
            # If the latest status flag is wontfix or fix-optional, we ignore
155
            # setting flags with the status "affected".
156
            is_latest_wontfix = self._is_latest_status_flag_wontfix(info)
1✔
157

158
            self.status_changes[bugid] = {}
1✔
159
            for channel in ("release", "beta", "central"):
1✔
160
                v = int(self.versions[channel])
1✔
161
                flag = utils.get_flag(v, "status", channel)
1✔
162
                info[channel] = info[flag]
1✔
163
                if info[flag] != "---":
1✔
164
                    # XXX maybe check for consistency?
165
                    continue
1✔
166
                if fixed_version is not None and v >= fixed_version:
1!
167
                    # Bug was fixed in an earlier version, don't set the flag
168
                    continue
×
169
                if v >= regressed_version:
1✔
170
                    if is_latest_wontfix:
1!
171
                        continue
×
172
                    self.status_changes[bugid][flag] = "affected"
1✔
173
                    info[channel] = "affected"
1✔
174
                else:
175
                    self.status_changes[bugid][flag] = "unaffected"
1✔
176
                    info[channel] = "unaffected"
1✔
177
                filtered_bugs[bugid] = info
1✔
178

179
            esr_versions = set([self.versions["esr"], self.versions["esr_previous"]])
1✔
180
            for v in esr_versions:
1✔
181
                info.setdefault("esr", {})
1✔
182
                flag = utils.get_flag(v, "status", "esr")
1✔
183
                if flag not in info:
1!
184
                    info["esr"][f"esr{v}"] = "n/a"
×
185
                    continue
×
186
                info["esr"][f"esr{v}"] = info[flag]
1✔
187
                if info[flag] != "---":
1!
188
                    # XXX maybe check for consistency?
189
                    continue
×
190
                if fixed_version is not None and int(v) >= fixed_version:
1!
191
                    # Bug was fixed in an earlier version, don't set the flag
192
                    continue
×
193
                if data[regressor].get(flag) in ("fixed", "verified"):
1✔
194
                    # regressor was uplifted, so the regression affects this branch
195
                    if is_latest_wontfix:
1!
196
                        continue
×
197
                    self.status_changes[bugid][flag] = "affected"
1✔
198
                    info["esr"][f"esr{v}"] = "affected"
1✔
199
                elif int(v) >= regressed_version:
1✔
200
                    # regression from before this branch, also affected
201
                    if is_latest_wontfix:
1!
202
                        continue
×
203
                    self.status_changes[bugid][flag] = "affected"
1✔
204
                    info["esr"][f"esr{v}"] = "affected"
1✔
205
                else:
206
                    self.status_changes[bugid][flag] = "unaffected"
1✔
207
                    info["esr"][f"esr{v}"] = "unaffected"
1✔
208
                filtered_bugs[bugid] = info
1✔
209

210
        # NOTE: The following is temporary to test that the new code is matching
211
        # producing the expected results. Once we are confident with the new
212
        # code, we can refactor this tool to use the new code.
213
        # See: https://github.com/mozilla/bugbot/issues/2152
214
        # >>>>> Begin of the temporary code
215
        adjusted_bugs = (
1✔
216
            {
217
                **bug,
218
                "regressed_by": [bug["regressed_by"]],
219
            }
220
            for bug in bugs.values()
221
        )
222
        bugs_store = BugsStore(
1✔
223
            chain(adjusted_bugs, data.values()), utils.get_checked_versions()
224
        )
225
        for bug_id, old_code_updates in self.status_changes.items():
1✔
226
            bug = bugs_store.get_bug_by_id(int(bug_id))
1✔
227
            try:
1✔
228
                new_code_updates = bug.detect_version_status_updates()
1✔
229
            except BugNotInStoreError as error:
×
230
                if self.dryrun:
×
231
                    # This should be OK in local environment where we don't have
232
                    # access to all bugs, but it is not OK in production.
233
                    continue
×
234
                raise error from None
×
235

236
            for update in new_code_updates:
1✔
237
                flag = update.flag
1✔
238
                if old_code_updates.get(flag) != update.status:
1!
239
                    logger.error(
×
240
                        "Rule %s - bug %s: Mismatching status for `%s`: %s <--> %s",
241
                        self.name(),
242
                        bug_id,
243
                        flag,
244
                        old_code_updates.get(flag),
245
                        update.status,
246
                    )
247
        # <<<<< End of the temporary code
248

249
        for bugid in filtered_bugs:
1✔
250
            regressor = bugs[bugid]["regressed_by"]
1✔
251
            self.status_changes[bugid]["comment"] = {
1✔
252
                "body": f"{self.description()} {regressor}",
253
                # if the regressing bug is private (security or otherwise
254
                # confidential), don't leak its number through our comment (the
255
                # regressed_by field is not public in that case)
256
                "is_private": bool(data[regressor].get("groups")),
257
            }
258
            self.status_changes[bugid]["keywords"] = {"add": ["regression"]}
1✔
259

260
        return filtered_bugs
1✔
261

262
    def get_bugs(self, *args, **kwargs):
1✔
263
        bugs = super().get_bugs(*args, **kwargs)
1✔
264
        bugs = self.get_status_changes(bugs)
1✔
265
        return bugs
1✔
266

267
    def get_autofix_change(self):
1✔
268
        return self.status_changes
×
269

270
    def columns(self):
1✔
271
        return ["id", "summary", "regressed_by", "central", "beta", "release", "esr"]
×
272

273

274
if __name__ == "__main__":
1!
275
    RegressionSetStatusFlags().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