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

mozilla / relman-auto-nag / #4604

pending completion
#4604

push

coveralls-python

suhaibmujahid
Some refactoring

Move the fetching of Bugzilla data to `SignaturesDataFetcher`
Move finding actionable crashes to `SignaturesDataFetcher`

646 of 3414 branches covered (18.92%)

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

1828 of 8480 relevant lines covered (21.56%)

0.22 hits per line

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

0.0
/bugbot/crash/analyzer.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
import itertools
×
6
import re
×
7
from collections import defaultdict
×
8
from datetime import timedelta
×
9
from functools import cached_property
×
10
from typing import Iterable, Iterator
×
11

12
from libmozdata import bugzilla, clouseau, connection, socorro
×
13
from libmozdata import utils as lmdutils
×
14
from libmozdata.bugzilla import Bugzilla
×
15
from libmozdata.connection import Connection
×
16

17
from bugbot import logger, utils
×
18
from bugbot.components import ComponentName
×
19
from bugbot.crash import socorro_util
×
20

21

22
# NOTE: At this point, we will file bugs on bugzilla-dev. Once we are confident
23
# that the bug filing is working as expected, we can switch to filing bugs in
24
# the production instance of Bugzilla.
25
class DevBugzilla(Bugzilla):
×
26
    URL = "https://bugzilla-dev.allizom.org"
×
27
    API_URL = URL + "/rest/bug"
×
28
    ATTACHMENT_API_URL = API_URL + "/attachment"
×
29
    TOKEN = utils.get_login_info()["bz_api_key_dev"]
×
30

31

32
class NoCrashReportFoundError(Exception):
×
33
    """Raised when no crash report is found with the required criteria."""
34

35

36
class ClouseauDataAnalyzer:
×
37
    """Analyze the data returned by Crash Clouseau"""
38

39
    MINIMUM_CLOUSEAU_SCORE_THRESHOLD: int = 8
×
40
    DEFAULT_CRASH_COMPONENT = ComponentName("Core", "General")
×
41

42
    def __init__(self, reports: Iterable[dict]):
×
43
        self._clouseau_reports = reports
×
44

45
    @cached_property
×
46
    def max_clouseau_score(self):
×
47
        """The maximum Clouseau score in the crash reports."""
48
        if not self._clouseau_reports:
×
49
            return 0
×
50
        return max(report["max_score"] for report in self._clouseau_reports)
×
51

52
    @cached_property
×
53
    def regressed_by_potential_bug_ids(self) -> set[int]:
×
54
        """The IDs for the bugs that their patches could have caused the crash."""
55
        minimum_accepted_score = max(
×
56
            self.MINIMUM_CLOUSEAU_SCORE_THRESHOLD, self.max_clouseau_score
57
        )
58
        return {
×
59
            changeset["bug_id"]
60
            for report in self._clouseau_reports
61
            if report["max_score"] >= minimum_accepted_score
62
            for changeset in report["changesets"]
63
            if changeset["max_score"] >= minimum_accepted_score
64
            and not changeset["is_merge"]
65
            and not changeset["is_backedout"]
66
        }
67

68
    @cached_property
×
69
    def regressed_by_patch(self) -> str | None:
×
70
        """The hash of the patch that could have caused the crash."""
71
        minimum_accepted_score = max(
×
72
            self.MINIMUM_CLOUSEAU_SCORE_THRESHOLD, self.max_clouseau_score
73
        )
74
        potential_patches = {
×
75
            changeset["changeset"]
76
            for report in self._clouseau_reports
77
            if report["max_score"] >= minimum_accepted_score
78
            for changeset in report["changesets"]
79
            if changeset["max_score"] >= minimum_accepted_score
80
            and not changeset["is_merge"]
81
            and not changeset["is_backedout"]
82
        }
83
        if len(potential_patches) == 1:
×
84
            return next(iter(potential_patches))
×
85
        return None
×
86

87
    @cached_property
×
88
    def regressed_by(self) -> int | None:
×
89
        """The ID of the bug that one of its patches could have caused
90
        the crash.
91

92
        If there are multiple bugs, the value will be `None`.
93
        """
94
        bug_ids = self.regressed_by_potential_bug_ids
×
95
        if len(bug_ids) == 1:
×
96
            return next(iter(bug_ids))
×
97
        return None
×
98

99
    @cached_property
×
100
    def regressed_by_potential_bugs(self) -> list[dict]:
×
101
        """The bugs that their patches could have caused the crash."""
102

103
        def handler(bug: dict, data: list):
×
104
            data.append(bug)
×
105

106
        bugs: list[dict] = []
×
107
        Bugzilla(
×
108
            bugids=self.regressed_by_potential_bug_ids,
109
            include_fields=[
110
                "id",
111
                "assigned_to",
112
                "product",
113
                "component",
114
            ],
115
            bughandler=handler,
116
            bugdata=bugs,
117
        ).wait()
118

119
        return bugs
×
120

121
    @cached_property
×
122
    def regressed_by_author(self) -> dict | None:
×
123
        """The author of the patch that could have caused the crash.
124

125
        If there are multiple regressors, the value will be `None`.
126

127
        The regressor bug assignee is considered as the author, even if the
128
        assignee is not the patch author.
129
        """
130

131
        if not self.regressed_by:
×
132
            return None
×
133

134
        bug = self.regressed_by_potential_bugs[0]
×
135
        assert bug["id"] == self.regressed_by
×
136
        return bug["assigned_to_detail"]
×
137

138
    @cached_property
×
139
    def crash_component(self) -> ComponentName:
×
140
        """The component that the crash belongs to.
141

142
        If there are multiple components, the value will be the default one.
143
        """
144
        potential_components = {
×
145
            ComponentName(bug["product"], bug["component"])
146
            for bug in self.regressed_by_potential_bugs
147
        }
148
        if len(potential_components) == 1:
×
149
            return next(iter(potential_components))
×
150
        return self.DEFAULT_CRASH_COMPONENT
×
151

152

153
class SocorroDataAnalyzer(socorro_util.SignatureStats):
×
154
    """Analyze the data returned by Socorro."""
155

156
    _bugzilla_os_legal_values = None
×
157
    _bugzilla_cpu_legal_values_map = None
×
158
    _platforms = [
×
159
        {"short_name": "win", "name": "Windows"},
160
        {"short_name": "mac", "name": "Mac OS X"},
161
        {"short_name": "lin", "name": "Linux"},
162
        {"short_name": "and", "name": "Android"},
163
        {"short_name": "unknown", "name": "Unknown"},
164
    ]
165

166
    def __init__(
×
167
        self,
168
        signature: dict,
169
        num_total_crashes: int,
170
    ):
171
        super().__init__(signature, num_total_crashes, platforms=self._platforms)
×
172

173
    @classmethod
×
174
    def to_bugzilla_op_sys(cls, op_sys: str) -> str:
×
175
        """Return the corresponding OS name in Bugzilla for the provide OS name
176
        from Socorro.
177

178
        If the OS name is not recognized, return "Other".
179
        """
180
        if cls._bugzilla_os_legal_values is None:
×
181
            cls._bugzilla_os_legal_values = set(
×
182
                bugzilla.BugFields.fetch_field_values("op_sys")
183
            )
184

185
        if op_sys in cls._bugzilla_os_legal_values:
×
186
            return op_sys
×
187

188
        if op_sys.startswith("OS X ") or op_sys.startswith("macOS "):
×
189
            op_sys = "macOS"
×
190
        elif op_sys.startswith("Windows"):
×
191
            op_sys = "Windows"
×
192
        elif "Linux" in op_sys or op_sys.startswith("Ubuntu"):
×
193
            op_sys = "Linux"
×
194
        else:
195
            op_sys = "Other"
×
196

197
        return op_sys
×
198

199
    @property
×
200
    def bugzilla_op_sys(self) -> str:
×
201
        """The name of the OS where the crash happens.
202

203
        The value is one of the legal values for Bugzilla's `op_sys` field.
204

205
        - If no OS name is found, the value will be "Unspecified".
206
        - If the OS name is not recognized, the value will be "Other".
207
        - If multiple OS names are found, the value will "All". Unless the OS
208
          names can be resolved common name without a version. For example,
209
          "Windows 10" and "Windows 7" will become "Windows".
210
        """
211
        all_op_sys = {
×
212
            self.to_bugzilla_op_sys(op_sys["term"])
213
            for op_sys in self.signature["facets"]["platform_pretty_version"]
214
        }
215

216
        if len(all_op_sys) > 1:
×
217
            # Resolve to root OS name by removing the version number.
218
            all_op_sys = {op_sys.split(" ")[0] for op_sys in all_op_sys}
×
219

220
        if len(all_op_sys) == 2 and "Other" in all_op_sys:
×
221
            # TODO: explain this workaround.
222
            all_op_sys.remove("Other")
×
223

224
        if len(all_op_sys) == 1:
×
225
            return next(iter(all_op_sys))
×
226

227
        if len(all_op_sys) == 0:
×
228
            return "Unspecified"
×
229

230
        return "All"
×
231

232
    @classmethod
×
233
    def to_bugzilla_cpu(cls, cpu: str) -> str:
×
234
        """Return the corresponding CPU name in Bugzilla for the provided name
235
        from Socorro.
236

237
        If the CPU is not recognized, return "Other".
238
        """
239
        if cls._bugzilla_cpu_legal_values_map is None:
×
240
            cls._bugzilla_cpu_legal_values_map = {
×
241
                value.lower(): value
242
                for value in bugzilla.BugFields.fetch_field_values("rep_platform")
243
            }
244

245
        return cls._bugzilla_cpu_legal_values_map.get(cpu, "Other")
×
246

247
    @property
×
248
    def bugzilla_cpu_arch(self) -> str:
×
249
        """The CPU architecture of the devices where the crash happens.
250

251
        The value is one of the legal values for Bugzilla's `rep_platform` field.
252

253
        - If no CPU architecture is found, the value will be "Unspecified".
254
        - If the CPU architecture is not recognized, the value will be "Other".
255
        - If multiple CPU architectures are found, the value will "All".
256
        """
257
        all_cpu_arch = {
×
258
            self.to_bugzilla_cpu(cpu["term"])
259
            for cpu in self.signature["facets"]["cpu_arch"]
260
        }
261

262
        if len(all_cpu_arch) == 2 and "Other" in all_cpu_arch:
×
263
            all_cpu_arch.remove("Other")
×
264

265
        if len(all_cpu_arch) == 1:
×
266
            return next(iter(all_cpu_arch))
×
267

268
        if len(all_cpu_arch) == 0:
×
269
            return "Unspecified"
×
270

271
        return "All"
×
272

273
    @property
×
274
    def num_user_comments(self) -> int:
×
275
        """The number crash reports with user comments."""
276
        # TODO: count useful/intrusting user comments (e.g., exclude one word comments)
277
        return self.signature["facets"]["cardinality_user_comments"]["value"]
×
278

279
    @property
×
280
    def has_user_comments(self) -> bool:
×
281
        """Whether the crash signature has any reports with a user comment."""
282
        return self.num_user_comments > 0
×
283

284
    @property
×
285
    def top_proto_signature(self) -> str:
×
286
        """The proto signature that occurs the most."""
287
        return self.signature["facets"]["proto_signature"][0]["term"]
×
288

289
    @property
×
290
    def num_top_proto_signature_crashes(self) -> int:
×
291
        """The number of crashes for the most occurring proto signature."""
292
        return self.signature["facets"]["proto_signature"][0]["count"]
×
293

294
    def _build_ids(self) -> Iterator[int]:
×
295
        """Yields the build IDs where the crash occurred."""
296
        for build_id in self.signature["facets"]["build_id"]:
×
297
            yield build_id["term"]
×
298

299
    @property
×
300
    def top_build_id(self) -> int:
×
301
        """The build ID where most crashes occurred."""
302
        return self.signature["facets"]["build_id"][0]["term"]
×
303

304

305
class SignatureAnalyzer(SocorroDataAnalyzer, ClouseauDataAnalyzer):
×
306
    """Analyze the data related to a signature.
307

308
    This includes data from Socorro and Clouseau.
309
    """
310

311
    def __init__(
×
312
        self,
313
        socorro_signature: dict,
314
        num_total_crashes: int,
315
        clouseau_reports: list[dict],
316
    ):
317
        SocorroDataAnalyzer.__init__(self, socorro_signature, num_total_crashes)
×
318
        ClouseauDataAnalyzer.__init__(self, clouseau_reports)
×
319

320
    def _fetch_crash_reports(
×
321
        self,
322
        proto_signature: str,
323
        build_id: int | Iterable[int],
324
        limit: int = 1,
325
    ) -> Iterator[dict]:
326
        params = {
×
327
            "proto_signature": "=" + proto_signature,
328
            "build_id": build_id,
329
            "_columns": [
330
                "uuid",
331
            ],
332
            "_results_number": limit,
333
        }
334

335
        def handler(res: dict, data: dict):
×
336
            data.update(res)
×
337

338
        data: dict = {}
×
339
        socorro.SuperSearch(params=params, handler=handler, handlerdata=data).wait()
×
340

341
        yield from data["hits"]
×
342

343
    def fetch_representing_processed_crash(self) -> dict:
×
344
        """Fetch a processed crash to represent the signature.
345

346
        This could fitch multiple processed crashes and return the one that is
347
        most likely to be useful.
348
        """
349
        limit_to_top_proto_signature = (
×
350
            self.num_top_proto_signature_crashes / self.num_crashes > 0.6
351
        )
352

353
        reports = itertools.chain(
×
354
            # Reports with a higher score from clouseau are more likely to be
355
            # useful.
356
            sorted(
357
                self._clouseau_reports,
358
                key=lambda report: report["max_score"],
359
                reverse=True,
360
            ),
361
            # Next we try find reports from the top crashing build because they
362
            # are likely to be representative.
363
            self._fetch_crash_reports(self.top_proto_signature, self.top_build_id),
364
            self._fetch_crash_reports(self.top_proto_signature, self._build_ids()),
365
        )
366
        for report in reports:
×
367
            uuid = report["uuid"]
×
368
            processed_crash = socorro.ProcessedCrash.get_processed(uuid)[uuid]
×
369
            if (
×
370
                not limit_to_top_proto_signature
371
                or processed_crash["proto_signature"] == self.top_proto_signature
372
            ):
373
                # TODO(investigate): maybe we should check if the stack is
374
                # corrupted (ask gsvelto or willkg about how to detect that)
375
                return processed_crash
×
376

377
        raise NoCrashReportFoundError(
×
378
            f"No crash report found with the most frequent proto signature for {self.signature_term}."
379
        )
380

381

382
class SignaturesDataFetcher:
×
383
    """Fetch the data related to the given signatures."""
384

385
    MEMORY_ACCESS_ERROR_REASONS = (
×
386
        # On Windows:
387
        "EXCEPTION_ACCESS_VIOLATION_READ",
388
        "EXCEPTION_ACCESS_VIOLATION_WRITE",
389
        "EXCEPTION_ACCESS_VIOLATION_EXEC"
390
        # On Linux:
391
        "SIGSEGV / SEGV_MAPERR",
392
        "SIGSEGV / SEGV_ACCERR",
393
    )
394

395
    EXCLUDED_MOZ_REASON_STRINGS = (
×
396
        "MOZ_CRASH(OOM)",
397
        "MOZ_CRASH(Out of memory)",
398
        "out of memory",
399
        "Shutdown hanging",
400
        # TODO(investigate): do we need to exclude signatures that their reason
401
        # contains `[unhandlable oom]`?
402
        # Example: arena_t::InitChunk | arena_t::AllocRun | arena_t::MallocLarge | arena_t::Malloc | BaseAllocator::malloc | Allocator::malloc | PageMalloc
403
        # "[unhandlable oom]",
404
    )
405

406
    # If any of the crash reason starts with any of the following, then it is
407
    # Network or I/O error.
408
    EXCLUDED_IO_ERROR_REASON_PREFIXES = (
×
409
        "EXCEPTION_IN_PAGE_ERROR_READ",
410
        "EXCEPTION_IN_PAGE_ERROR_WRITE",
411
        "EXCEPTION_IN_PAGE_ERROR_EXEC",
412
    )
413

414
    # TODO(investigate): do we need to exclude all these signatures prefixes?
415
    EXCLUDED_SIGNATURE_PREFIXES = (
×
416
        "OOM | ",
417
        "bad hardware | ",
418
        "shutdownhang | ",
419
    )
420

421
    def __init__(
×
422
        self,
423
        signatures: Iterable[str],
424
        product: str = "Firefox",
425
        channel: str = "nightly",
426
    ):
427
        self._signatures = set(signatures)
×
428
        self._product = product
×
429
        self._channel = channel
×
430

431
    @classmethod
×
432
    def find_new_actionable_crashes(
×
433
        cls,
434
        product="Firefox",
435
        channel="nightly",
436
        days_to_check=7,
437
        days_without_crashes=7,
438
    ):
439
        duration = days_to_check + days_without_crashes
×
440
        end_date = lmdutils.get_date_ymd("today")
×
441
        start_date = end_date - timedelta(duration)
×
442
        earliest_allowed_date = lmdutils.get_date_str(
×
443
            end_date - timedelta(days_to_check)
444
        )
445
        date_range = socorro.SuperSearch.get_search_date(start_date, end_date)
×
446

447
        params = {
×
448
            "product": product,
449
            "release_channel": channel,
450
            "date": date_range,
451
            # TODO(investigate): should we do a local filter instead of the
452
            # following (should we exclude the signature if one of the crashes
453
            # is a shutdown hang?):
454
            # If the `ipc_shutdown_state` or `shutdown_progress` field are
455
            # non-empty then it's a shutdown hang.
456
            "ipc_shutdown_state": "__null__",
457
            "shutdown_progress": "__null__",
458
            # TODO(investigate): should we use the following instead of the
459
            # local filter.
460
            # "oom_allocation_size": "!__null__",
461
            "_aggs.signature": [
462
                "moz_crash_reason",
463
                "reason",
464
                "_histogram.date",
465
                "_cardinality.install_time",
466
                "_cardinality.oom_allocation_size",
467
            ],
468
            "_results_number": 0,
469
            "_facets_size": 10000,
470
        }
471

472
        def handler(search_resp: dict, data: list):
×
473
            logger.debug(
×
474
                "Total of %d signatures received from Socorro",
475
                len(search_resp["facets"]["signature"]),
476
            )
477

478
            for crash in search_resp["facets"]["signature"]:
×
479
                signature = crash["term"]
×
480
                if any(
×
481
                    signature.startswith(excluded_prefix)
482
                    for excluded_prefix in cls.EXCLUDED_SIGNATURE_PREFIXES
483
                ):
484
                    # Ignore signatures that start with any of the excluded prefixes.
485
                    continue
×
486

487
                facets = crash["facets"]
×
488
                installations = facets["cardinality_install_time"]["value"]
×
489
                if installations <= 1:
×
490
                    # Ignore crashes that only happen on one installation.
491
                    continue
×
492

493
                first_date = facets["histogram_date"][0]["term"]
×
494
                if first_date < earliest_allowed_date:
×
495
                    # The crash is not new, skip it.
496
                    continue
×
497

498
                if any(
×
499
                    reason["term"].startswith(io_error_prefix)
500
                    for reason in facets["reason"]
501
                    for io_error_prefix in cls.EXCLUDED_IO_ERROR_REASON_PREFIXES
502
                ):
503
                    # Ignore Network or I/O error crashes.
504
                    continue
×
505

506
                if crash["count"] < 20:
×
507
                    # For signatures with low volume, having multiple types of
508
                    # memory errors indicates potential bad hardware crashes.
509
                    num_memory_error_types = sum(
×
510
                        reason["term"] in cls.MEMORY_ACCESS_ERROR_REASONS
511
                        for reason in facets["reason"]
512
                    )
513
                    if num_memory_error_types > 1:
×
514
                        # Potential bad hardware crash, skip it.
515
                        continue
×
516

517
                # TODO: Add a filter using the `possible_bit_flips_max_confidence`
518
                # field to exclude bad hardware crashes. The filed is not available yet.
519
                # See: https://bugzilla.mozilla.org/show_bug.cgi?id=1816669#c3
520

521
                # TODO(investigate): is this needed since we are already
522
                # filtering signatures that start with "OOM | "
523
                if facets["cardinality_oom_allocation_size"]["value"]:
×
524
                    # If one of the crashes is an OOM crash, skip it.
525
                    continue
×
526

527
                # TODO(investigate): do we need to check for the `moz_crash_reason`
528
                moz_crash_reasons = facets["moz_crash_reason"]
×
529
                if moz_crash_reasons and any(
×
530
                    excluded_reason in reason["term"]
531
                    for reason in moz_crash_reasons
532
                    for excluded_reason in cls.EXCLUDED_MOZ_REASON_STRINGS
533
                ):
534
                    continue
×
535

536
                data.append(signature)
×
537

538
        signatures: list = []
×
539
        socorro.SuperSearch(
×
540
            params=params,
541
            handler=handler,
542
            handlerdata=signatures,
543
        ).wait()
544

545
        logger.debug(
×
546
            "Total of %d signatures left after applying the filtering criteria",
547
            len(signatures),
548
        )
549

550
        return cls(signatures, product, channel)
×
551

552
    def fetch_clouseau_crash_reports(self) -> dict[str, list]:
×
553
        """Fetch the crash reports data from Crash Clouseau."""
554
        signature_reports = clouseau.Reports.get_by_signatures(
×
555
            self._signatures,
556
            product=self._product,
557
            channel=self._channel,
558
        )
559

560
        logger.debug(
×
561
            "Total of %d signatures received from Clouseau", len(signature_reports)
562
        )
563

564
        return signature_reports
×
565

566
    def fetch_socorro_info(self) -> tuple[list[dict], int]:
×
567
        """Fetch the signature data from Socorro."""
568
        # TODO(investigate): should we increase the duration to 6 months?
569
        duration = timedelta(weeks=1)
×
570
        end_date = lmdutils.get_date_ymd("today")
×
571
        start_date = end_date - duration
×
572
        date_range = socorro.SuperSearch.get_search_date(start_date, end_date)
×
573

574
        params = {
×
575
            "product": self._product,
576
            # TODO(investigate): should we included all release channels?
577
            "release_channel": self._channel,
578
            # TODO(investigate): should we limit based on the build date as well?
579
            "date": date_range,
580
            # TODO: split signatures into chunks to avoid very long query URLs
581
            "signature": ["=" + signature for signature in self._signatures],
582
            "_aggs.signature": [
583
                "build_id",
584
                "cpu_arch",
585
                "proto_signature",
586
                "_cardinality.user_comments",
587
                "cpu_arch",
588
                "platform_pretty_version",
589
                # The following are needed for SignatureStats:
590
                "platform",
591
                "is_garbage_collecting",
592
                "_cardinality.install_time",
593
                "startup_crash",
594
                "_histogram.uptime",
595
                "process_type",
596
            ],
597
            "_results_number": 0,
598
            "_facets_size": 10000,
599
        }
600

601
        def handler(search_results: dict, data: dict):
×
602
            data["num_total_crashes"] = search_results["total"]
×
603
            data["signatures"] = search_results["facets"]["signature"]
×
604

605
        data: dict = {}
×
606
        socorro.SuperSearchUnredacted(
×
607
            params=params,
608
            handler=handler,
609
            handlerdata=data,
610
        ).wait()
611

612
        logger.debug(
×
613
            "Fetch info from Socorro for %d signatures", len(data["signatures"])
614
        )
615

616
        return data["signatures"], data["num_total_crashes"]
×
617

618
    def fetch_bugs(self, include_fields: list[str] = None) -> dict[str, list[dict]]:
×
619
        """Fetch bugs that are filed against the given signatures."""
620

621
        params_base: dict = {
×
622
            "include_fields": [
623
                "cf_crash_signature",
624
            ],
625
        }
626

627
        if include_fields:
×
628
            params_base["include_fields"].extend(include_fields)
×
629

630
        params_list = []
×
631
        for signatures_chunk in Connection.chunks(list(self._signatures), 30):
×
632
            params = params_base.copy()
×
633
            n = int(utils.get_last_field_num(params))
×
634
            params[f"f{n}"] = "OP"
×
635
            params[f"j{n}"] = "OR"
×
636
            for signature in signatures_chunk:
×
637
                n += 1
×
638
                params[f"f{n}"] = "cf_crash_signature"
×
639
                params[f"o{n}"] = "regexp"
×
640
                params[f"v{n}"] = rf"\[(@ |@){re.escape(signature)}( \]|\])"
×
641
            params[f"f{n+1}"] = "CP"
×
642
            params_list.append(params)
×
643

644
        signatures_bugs: dict = defaultdict(list)
×
645

646
        def handler(res, data):
×
647
            for bug in res["bugs"]:
×
648
                for signature in utils.get_signatures(bug["cf_crash_signature"]):
×
649
                    data[signature].append(bug)
×
650

651
        Bugzilla(
×
652
            queries=[
653
                connection.Query(Bugzilla.API_URL, params, handler, signatures_bugs)
654
                for params in params_list
655
            ],
656
        ).wait()
657

658
        # TODO: remove the call to DevBugzilla after moving to production
659
        DevBugzilla(
×
660
            queries=[
661
                connection.Query(DevBugzilla.API_URL, params, handler, signatures_bugs)
662
                for params in params_list
663
            ],
664
        ).wait()
665

666
        logger.debug(
×
667
            "Total of %d signatures already have bugs filed", len(signatures_bugs)
668
        )
669

670
        return signatures_bugs
×
671

672
    def analyze(self) -> list[SignatureAnalyzer]:
×
673
        """Analyze the data related to the signatures."""
674
        bugs = self.fetch_bugs()
×
675
        # TODO(investigate): For now, we are ignoring signatures that have bugs
676
        # filed even if they are closed long time ago. We should investigate
677
        # whether we should include the ones with closed bugs. For example, if
678
        # the bug was closed as Fixed years ago.
679
        self._signatures.difference_update(bugs.keys())
×
680

681
        clouseau_reports = self.fetch_clouseau_crash_reports()
×
682
        # TODO(investigate): For now, we are ignoring signatures that are not
683
        # analyzed by clouseau. We should investigate why they are not analyzed
684
        # and whether we should include them.
685
        self._signatures.intersection_update(clouseau_reports.keys())
×
686

687
        signatures, num_total_crashes = self.fetch_socorro_info()
×
688
        logger.debug("Total of %d signatures will be analyzed", len(signatures))
×
689

690
        return [
×
691
            SignatureAnalyzer(
692
                signature,
693
                num_total_crashes,
694
                clouseau_reports[signature["term"]],
695
            )
696
            for signature in signatures
697
        ]
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