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

mozilla / mozregression / 11572770404

29 Oct 2024 11:18AM CUT coverage: 35.088%. First build
11572770404

Pull #1450

github

web-flow
Merge 653740e1e into f558f7daa
Pull Request #1450: Bug 1763188 - Add Snap support using TC builds

62 of 193 new or added lines in 7 files covered. (32.12%)

1053 of 3001 relevant lines covered (35.09%)

1.05 hits per line

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

22.59
/mozregression/bisector.py
1
from __future__ import absolute_import
3✔
2

3
import math
3✔
4
import os
3✔
5
import threading
3✔
6
from abc import ABCMeta, abstractmethod
3✔
7

8
from mozlog import get_proxy_logger
3✔
9

10
from mozregression.branches import find_branch_in_merge_commit, get_name
3✔
11
from mozregression.build_range import get_integration_range, get_nightly_range
3✔
12
from mozregression.dates import to_datetime
3✔
13
from mozregression.errors import (
3✔
14
    EmptyPushlogError,
15
    GoodBadExpectationError,
16
    LauncherError,
17
    MozRegressionError,
18
)
19
from mozregression.history import BisectionHistory
3✔
20
from mozregression.json_pushes import JsonPushes
3✔
21

22
LOG = get_proxy_logger("Bisector")
3✔
23

24

25
def compute_steps_left(steps):
3✔
26
    if steps <= 1:
×
27
        return 0
×
28
    return math.trunc(math.log(steps, 2))
×
29

30

31
class BisectorHandler(metaclass=ABCMeta):
3✔
32
    """
33
    React to events of a :class:`Bisector`. This is intended to be subclassed.
34

35
    A BisectorHandler keep the state of the current bisection process.
36
    """
37

38
    def __init__(self, find_fix=False, ensure_good_and_bad=False):
3✔
39
        self.find_fix = find_fix
×
40
        self.ensure_good_and_bad = ensure_good_and_bad
×
41
        self.found_repo = None
×
42
        self.build_range = None
×
43
        self.good_revision = None
×
44
        self.bad_revision = None
×
45

46
    def set_build_range(self, build_range):
3✔
47
        """
48
        Save a reference of the :class:`mozregression.build_range.BuildData`
49
        instance.
50

51
        This is called by the bisector before each step of the bisection
52
        process.
53
        """
54
        self.build_range = build_range
×
55

56
    @abstractmethod
3✔
57
    def _print_progress(self, new_data):
3✔
58
        """
59
        Log the current state of the bisection process.
60
        """
61
        raise NotImplementedError
62

63
    def _reverse_if_find_fix(self, var1, var2):
3✔
64
        return (var1, var2) if not self.find_fix else (var2, var1)
×
65

66
    def initialize(self):
3✔
67
        """
68
        Initialize some data at the beginning of each step of a bisection
69
        process.
70

71
        This will only be called if there is some build data.
72
        """
73
        # these values could be missing for old integration builds
74
        # until we tried the builds
75
        repo = self.build_range[-1].repo_url
×
76
        if repo is not None:
×
77
            # do not update repo if we can' find it now
78
            # else we may override a previously defined one
79
            self.found_repo = repo
×
80
        self.good_revision, self.bad_revision = self._reverse_if_find_fix(
×
81
            self.build_range[0].changeset, self.build_range[-1].changeset
82
        )
83

84
    def get_pushlog_url(self):
3✔
85
        first_rev, last_rev = self.get_range()
×
86
        if first_rev == last_rev:
×
87
            return "%s/pushloghtml?changeset=%s" % (self.found_repo, first_rev)
×
88
        return "%s/pushloghtml?fromchange=%s&tochange=%s" % (
×
89
            self.found_repo,
90
            first_rev,
91
            last_rev,
92
        )
93

94
    def get_range(self):
3✔
95
        return self._reverse_if_find_fix(self.good_revision, self.bad_revision)
×
96

97
    def print_range(self, good_date=None, bad_date=None, full=True):
3✔
98
        """
99
        Log the state of the current state of the bisection process, with an
100
        appropriate pushlog url.
101
        """
102
        if full:
×
103
            if good_date and bad_date:
×
104
                good_date = " (%s)" % good_date
×
105
                bad_date = " (%s)" % bad_date
×
106
            words = self._reverse_if_find_fix("Last", "First")
×
107
            LOG.info(
×
108
                "%s good revision: %s%s"
109
                % (words[0], self.good_revision, good_date if good_date else "")
110
            )
111
            LOG.info(
×
112
                "%s bad revision: %s%s"
113
                % (words[1], self.bad_revision, bad_date if bad_date else "")
114
            )
115
        LOG.info("Pushlog:\n%s\n" % self.get_pushlog_url())
×
116

117
    def build_good(self, mid, new_data):
3✔
118
        """
119
        Called by the Bisector when a build is good.
120

121
        *new_data* is ensured to contain at least two elements.
122
        """
123
        self._print_progress(new_data)
×
124

125
    def build_bad(self, mid, new_data):
3✔
126
        """
127
        Called by the Bisector when a build is bad.
128

129
        *new_data* is ensured to contain at least two elements.
130
        """
131
        self._print_progress(new_data)
×
132

133
    def build_retry(self, mid):
3✔
134
        pass
135

136
    def build_skip(self, mid):
3✔
137
        pass
138

139
    def no_data(self):
3✔
140
        pass
141

142
    def finished(self):
3✔
143
        pass
144

145
    def user_exit(self, mid):
3✔
146
        pass
147

148

149
class NightlyHandler(BisectorHandler):
3✔
150
    create_range = staticmethod(get_nightly_range)
3✔
151
    good_date = None
3✔
152
    bad_date = None
3✔
153

154
    def initialize(self):
3✔
155
        BisectorHandler.initialize(self)
×
156
        # register dates
157
        self.good_date, self.bad_date = self._reverse_if_find_fix(
×
158
            self.build_range[0].build_date, self.build_range[-1].build_date
159
        )
160

161
    def _print_progress(self, new_data):
3✔
162
        good_date, bad_date = self._reverse_if_find_fix(self.good_date, self.bad_date)
×
163
        next_good_date = new_data[0].build_date
×
164
        next_bad_date = new_data[-1].build_date
×
165
        next_days_range = abs((to_datetime(next_bad_date) - to_datetime(next_good_date)).days)
×
166
        LOG.info(
×
167
            "Narrowed nightly %s window from"
168
            " [%s, %s] (%d days) to [%s, %s] (%d days)"
169
            " (~%d steps left)"
170
            % (
171
                "fix" if self.find_fix else "regression",
172
                good_date,
173
                bad_date,
174
                abs((to_datetime(self.bad_date) - to_datetime(self.good_date)).days),
175
                next_good_date,
176
                next_bad_date,
177
                next_days_range,
178
                compute_steps_left(next_days_range),
179
            )
180
        )
181

182
    def _print_date_range(self):
3✔
183
        words = self._reverse_if_find_fix("Newest", "Oldest")
×
184
        LOG.info("%s known good nightly: %s" % (words[0], self.good_date))
×
185
        LOG.info("%s known bad nightly: %s" % (words[1], self.bad_date))
×
186

187
    def user_exit(self, mid):
3✔
188
        self._print_date_range()
×
189

190
    def are_revisions_available(self):
3✔
191
        return self.good_revision is not None and self.bad_revision is not None
×
192

193
    def get_date_range(self):
3✔
194
        return self._reverse_if_find_fix(self.good_date, self.bad_date)
×
195

196
    def print_range(self, full=True):
3✔
197
        if self.found_repo is None:
×
198
            # this may happen if we are bisecting old builds without
199
            # enough tests of the builds.
200
            LOG.error(
×
201
                "Sorry, but mozregression was unable to get"
202
                " a repository - no pushlog url available."
203
            )
204
            # still we can print date range
205
            if full:
×
206
                self._print_date_range()
×
207
        elif self.are_revisions_available():
×
208
            BisectorHandler.print_range(self, self.good_date, self.bad_date, full=full)
×
209
        else:
210
            if full:
×
211
                self._print_date_range()
×
212
            LOG.info("Pushlog:\n%s\n" % self.get_pushlog_url())
×
213

214
    def get_pushlog_url(self):
3✔
215
        assert self.found_repo
×
216
        if self.are_revisions_available():
×
217
            return BisectorHandler.get_pushlog_url(self)
×
218
        else:
219
            start, end = self.get_date_range()
×
220
            return "%s/pushloghtml?startdate=%s&enddate=%s\n" % (
×
221
                self.found_repo,
222
                start,
223
                end,
224
            )
225

226

227
class IntegrationHandler(BisectorHandler):
3✔
228
    create_range = staticmethod(get_integration_range)
3✔
229

230
    def _print_progress(self, new_data):
3✔
231
        LOG.info(
×
232
            "Narrowed integration %s window from [%s, %s]"
233
            " (%d builds) to [%s, %s] (%d builds)"
234
            " (~%d steps left)"
235
            % (
236
                "fix" if self.find_fix else "regression",
237
                self.build_range[0].short_changeset,
238
                self.build_range[-1].short_changeset,
239
                len(self.build_range),
240
                new_data[0].short_changeset,
241
                new_data[-1].short_changeset,
242
                len(new_data),
243
                compute_steps_left(len(new_data)),
244
            )
245
        )
246

247
    def user_exit(self, mid):
3✔
248
        words = self._reverse_if_find_fix("Newest", "Oldest")
×
249
        LOG.info("%s known good integration revision: %s" % (words[0], self.good_revision))
×
250
        LOG.info("%s known bad integration revision: %s" % (words[1], self.bad_revision))
×
251

252
    def _choose_integration_branch(self, changeset):
3✔
253
        """
254
        Tries to determine which integration branch the given changeset
255
        originated from by checking the date the changeset first showed up
256
        in each repo. The repo with the earliest date is chosen.
257
        """
258
        landings = {}
×
259
        for k in ("autoland", "mozilla-inbound"):
×
260
            jp = JsonPushes(k)
×
261

262
            try:
×
263
                push = jp.push(changeset, full="1")
×
264
                landings[k] = push.timestamp
×
265
            except EmptyPushlogError:
×
266
                LOG.debug("Didn't find %s in %s" % (changeset, k))
×
267

268
        repo = min(landings, key=landings.get)
×
269
        LOG.debug("Repo '%s' seems to have the earliest push" % repo)
×
270
        return repo
×
271

272
    def handle_merge(self):
3✔
273
        # let's check if we are facing a merge, and in that case,
274
        # continue the bisection from the merged branch.
275
        result = None
×
276

277
        LOG.debug("Starting merge handling...")
×
278
        # we have to check the commit of the most recent push
279
        most_recent_push = self.build_range[1]
×
280
        jp = JsonPushes(most_recent_push.repo_name)
×
281
        push = jp.push(most_recent_push.changeset, full="1")
×
282
        msg = push.changeset["desc"]
×
283
        LOG.debug("Found commit message:\n%s\n" % msg)
×
284
        branch = find_branch_in_merge_commit(msg, most_recent_push.repo_name)
×
285
        if not (branch and len(push.changesets) >= 2):
×
286
            # We did not find a branch, lets check the integration branches if we are bisecting m-c
287
            LOG.debug("Did not find a branch, checking all integration branches")
×
288
            if (
×
289
                get_name(most_recent_push.repo_name) == "mozilla-central"
290
                and len(push.changesets) >= 2
291
            ):
292
                branch = self._choose_integration_branch(most_recent_push.changeset)
×
293
                oldest = push.changesets[0]["node"]
×
294
                youngest = push.changesets[-1]["node"]
×
295
                LOG.info(
×
296
                    "************* Switching to %s by"
297
                    " process of elimination (no branch detected in"
298
                    " commit message)" % branch
299
                )
300
            else:
301
                return
×
302
        else:
303
            # so, this is a merge. see how many changesets are in it, if it
304
            # is just one, we have our answer
305
            if len(push.changesets) == 2:
×
306
                LOG.info(
×
307
                    "Merge commit has only two revisions (one of which "
308
                    "is the merge): we are done"
309
                )
310
                return
×
311

312
            # Otherwise, we can find the oldest and youngest
313
            # changesets, and the branch where the merge comes from.
314
            oldest = push.changesets[0]["node"]
×
315
            # exclude the merge commit
316
            youngest = push.changesets[-2]["node"]
×
317
            LOG.info("************* Switching to %s" % branch)
×
318

319
        # we can't use directly the oldest changeset because we
320
        # don't know yet if it is good.
321
        #
322
        # PUSH1    PUSH2
323
        # [1 2] [3 4 5 6 7]
324
        #    G    MERGE  B
325
        #
326
        # so first grab the previous push to get the last known good
327
        # changeset. This needs to be done on the right branch.
328
        try:
×
329
            jp2 = JsonPushes(branch)
×
330
            raw = [int(p.push_id) for p in jp2.pushes_within_changes(oldest, youngest)]
×
331
            data = jp2.pushes(
×
332
                startID=str(min(raw) - 2),
333
                endID=str(max(raw)),
334
            )
335

336
            older = data[0].changeset
×
337
            youngest = data[-1].changeset
×
338

339
            # we are ready to bisect further
340
            gr, br = self._reverse_if_find_fix(older, youngest)
×
341
            result = (branch, gr, br)
×
342
        except MozRegressionError:
×
343
            LOG.debug("Got exception", exc_info=True)
×
344
            raise MozRegressionError(
×
345
                "Unable to exploit the merge commit. Origin branch is {}, and"
346
                " the commit message for {} was:\n{}".format(
347
                    most_recent_push.repo_name, most_recent_push.short_changeset, msg
348
                )
349
            )
350
        LOG.debug("End merge handling")
×
351
        return result
×
352

353

354
class SnapHandler(IntegrationHandler):
3✔
355
    """
356
    Snap packages for all snap branches are built from cron jobs on
357
    mozilla-central, so it maps into the IntegrationHandler as per the
358
    definition of mozregression.
359

360
    All branches of the snap package have their own pinning of the source
361
    repository / source code information, so the changeset on mozilla-central
362
    and other informations are not relevant and it is required to extract it
363
    from somewhere else (e.g., application.ini).
364

365
    There are also no merge to account for so handle_merge() is a no-op.
366
    """
367

368
    snap_repo = None
3✔
369
    _build_infos = {}
3✔
370
    snap_rev = {}
3✔
371

372
    def record_build_infos(self, build_infos):
3✔
373
        """
374
        The way the build infos is produced on mozilla-central does not match
375
        requirements of snap package: it is required that the information is
376
        extracted from a different source since all branches of the snap
377
        package are built from mozilla-central. Without this, then the buildid,
378
        changeset and repository informations are matching mozilla-central when
379
        the build was triggered and not what the snap package was built from.
380
        """
NEW
381
        self._build_infos["_changeset"] = build_infos._changeset
×
NEW
382
        self._build_infos["_repo_url"] = build_infos._repo_url
×
NEW
383
        self.snap_repo = build_infos._repo_url
×
384

385
    def update_build_infos(self, build_infos):
3✔
386
        """
387
        Keep track of the Snap-specific build infos that have been collected
388
        and given in parameter in build_infos, and keep a copy of the
389
        mozilla-central ones within _build_infos if it is required to revert
390
        later.
391
        """
NEW
392
        self.snap_rev[self._build_infos["_changeset"]] = build_infos.changeset
×
NEW
393
        self.snap_repo = build_infos._repo_url
×
394

395
    def get_pushlog_url(self):
3✔
396
        # somehow, self.found_repo from this class would not reflect
NEW
397
        first_rev, last_rev = self.get_range()
×
NEW
398
        if first_rev == last_rev:
×
NEW
399
            return "%s/pushloghtml?changeset=%s" % (self.snap_repo, first_rev)
×
NEW
400
        return "%s/pushloghtml?fromchange=%s&tochange=%s" % (
×
401
            self.snap_repo,
402
            first_rev,
403
            last_rev,
404
        )
405

406
    def revert_build_infos(self, build_infos):
3✔
407
        """
408
        Some old Snap nightly builds are missing SourceRepository/SourceStamp
409
        Since there is not a better source of information, revert to the
410
        informations provided by mozilla-central.
411
        """
NEW
412
        build_infos._changeset = self._build_infos["_changeset"]
×
NEW
413
        build_infos._repo_url = self._build_infos["_repo_url"]
×
414

415
    def handle_merge(self):
3✔
416
        """
417
        No-op by definition of how the snap packages are built on
418
        mozilla-central cron jobs.
419
        """
NEW
420
        return None
×
421

422

423
class IndexPromise(object):
3✔
424
    """
425
    A promise to get a build index.
426

427
    Provide a callable object that gives the next index when called.
428
    """
429

430
    def __init__(self, index, callback=None, args=()):
3✔
431
        self.thread = None
×
432
        self.index = index
×
433
        if callback:
×
434
            self.thread = threading.Thread(target=self._run, args=(callback,) + args)
×
435
            self.thread.start()
×
436

437
    def _run(self, callback, *args):
3✔
438
        self.index = callback(self.index, *args)
×
439

440
    def __call__(self):
3✔
441
        if self.thread:
×
442
            self.thread.join()
×
443
        return self.index
×
444

445

446
class Bisection(object):
3✔
447
    RUNNING = 0
3✔
448
    NO_DATA = 1
3✔
449
    FINISHED = 2
3✔
450
    USER_EXIT = 3
3✔
451

452
    def __init__(
3✔
453
        self,
454
        handler,
455
        build_range,
456
        download_manager,
457
        test_runner,
458
        dl_in_background=True,
459
        approx_chooser=None,
460
    ):
461
        self.handler = handler
×
462
        self.build_range = build_range
×
463
        self.download_manager = download_manager
×
464
        self.test_runner = test_runner
×
465
        self.dl_in_background = dl_in_background
×
466
        self.history = BisectionHistory()
×
467
        self.approx_chooser = approx_chooser
×
468

469
    def search_mid_point(self, interrupt=None):
3✔
470
        self.handler.set_build_range(self.build_range)
×
471
        return self._search_mid_point(interrupt=interrupt)
×
472

473
    def _search_mid_point(self, interrupt=None):
3✔
474
        return self.build_range.mid_point(interrupt=interrupt)
×
475

476
    def init_handler(self, mid_point):
3✔
477
        if len(self.build_range) == 0:
×
478
            self.handler.no_data()
×
479
            return self.NO_DATA
×
480

481
        self.handler.initialize()
×
482

483
        if mid_point == 0:
×
484
            self.handler.finished()
×
485
            return self.FINISHED
×
486
        return self.RUNNING
×
487

488
    def download_build(self, mid_point, allow_bg_download=True):
3✔
489
        """
490
        Download the build for the given mid_point.
491

492
        This call may start the download of next builds in background (if
493
        dl_in_background evaluates to True). Note that the mid point may
494
        change in this case.
495

496
        Returns a couple (index_promise, build_infos) where build_infos
497
        is the dict of build infos for the build.
498
        """
499
        build_infos = self.handler.build_range[mid_point]
×
500
        return self._download_build(mid_point, build_infos, allow_bg_download=allow_bg_download)
×
501

502
    def _find_approx_build(self, mid_point, build_infos):
3✔
503
        approx_index, persist_files = None, ()
×
504
        if self.approx_chooser:
×
505
            # try to find an approx build
506
            persist_files = os.listdir(self.download_manager.destdir)
×
507
            # first test if we have the exact file - if we do,
508
            # just act as usual, the downloader will take care of it.
509
            if build_infos.persist_filename not in persist_files:
×
510
                approx_index = self.approx_chooser.index(
×
511
                    self.build_range, build_infos, persist_files
512
                )
513
        if approx_index is not None:
×
514
            # we found an approx build. First, stop possible background
515
            # downloads, then update the mid point and build info.
516
            if self.download_manager.background_dl_policy == "cancel":
×
517
                self.download_manager.cancel()
×
518

519
            old_url = build_infos.build_url
×
520
            mid_point = approx_index
×
521
            build_infos = self.build_range[approx_index]
×
522
            fname = self.download_manager.get_dest(build_infos.persist_filename)
×
523
            LOG.info(
×
524
                "Using `%s` as an acceptable approximated"
525
                " build file instead of downloading %s" % (fname, old_url)
526
            )
527
            build_infos.build_file = fname
×
528
        return (approx_index is not None, mid_point, build_infos, persist_files)
×
529

530
    def _download_build(self, mid_point, build_infos, allow_bg_download=True):
3✔
531
        found, mid_point, build_infos, persist_files = self._find_approx_build(
×
532
            mid_point, build_infos
533
        )
534
        if not found and self.download_manager:
×
535
            # else, do the download. Note that nothing will
536
            # be downloaded if the exact build file is already present.
537
            self.download_manager.focus_download(build_infos)
×
538
        callback = None
×
539
        if self.dl_in_background and allow_bg_download:
×
540
            callback = self._download_next_builds
×
541
        return (IndexPromise(mid_point, callback, args=(persist_files,)), build_infos)
×
542

543
    def _download_next_builds(self, mid_point, persist_files=()):
3✔
544
        # start downloading the next builds.
545
        # note that we don't have to worry if builds are already
546
        # downloaded, or if our build infos are the same because
547
        # this will be handled by the downloadmanager.
548
        def start_dl(r):
×
549
            # first get the next mid point
550
            # this will trigger some blocking downloads
551
            # (we need to find the build info)
552
            m = r.mid_point()
×
553
            if len(r) != 0:
×
554
                # non-blocking download of the build
555
                if (
×
556
                    self.approx_chooser
557
                    and self.approx_chooser.index(r, r[m], persist_files) is not None
558
                ):
559
                    pass  # nothing to download, we have an approx build
560
                else:
561
                    self.download_manager.download_in_background(r[m])
×
562

563
        bdata = self.build_range[mid_point]
×
564
        # download next left mid point
565
        start_dl(self.build_range[mid_point:])
×
566
        # download right next mid point
567
        start_dl(self.build_range[: mid_point + 1])
×
568
        # since we called mid_point() on copy of self.build_range instance,
569
        # the underlying cache may have changed and we need to find the new
570
        # mid point.
571
        self.build_range.filter_invalid_builds()
×
572
        return self.build_range.index(bdata)
×
573

574
    def evaluate(self, build_infos):
3✔
575
        # we force getting data from app info for snap since we are building everything
576
        # out of mozilla-central
NEW
577
        if isinstance(self.handler, SnapHandler):
×
NEW
578
            self.handler.record_build_infos(build_infos)
×
579
        verdict = self.test_runner.evaluate(build_infos, allow_back=bool(self.history))
×
580
        # old builds do not have metadata about the repo. But once
581
        # the build is installed, we may have it
582
        if self.handler.found_repo is None:
×
583
            self.handler.found_repo = build_infos.repo_url
×
NEW
584
        if isinstance(self.handler, SnapHandler):
×
585
            # Some Snap nightly builds are missing SourceRepository/SourceStamp
586
            # So since we dont have a better source of information, let's get back
587
            # what we had
NEW
588
            if build_infos.repo_url is None:
×
NEW
589
                LOG.warning(
×
590
                    "Bisection on a Snap package missing SourceRepository/SourceStamp,"
591
                    " falling back to mozilla-central revs."
592
                )
NEW
593
                self.handler.revert_build_infos(build_infos)
×
594
            else:
NEW
595
                self.handler.update_build_infos(build_infos)
×
596
        return verdict
×
597

598
    def ensure_good_and_bad(self):
3✔
599
        good, bad = self.build_range[0], self.build_range[-1]
×
600
        if self.handler.find_fix:
×
601
            good, bad = bad, good
×
602

603
        LOG.info("Testing good and bad builds to ensure that they are" " really good and bad...")
×
604
        self.download_manager.focus_download(good)
×
605
        if self.dl_in_background:
×
606
            self.download_manager.download_in_background(bad)
×
607

608
        def _evaluate(build_info, expected):
×
609
            while 1:
610
                res = self.test_runner.evaluate(build_info)
×
611
                if res == expected[0]:
×
612
                    return True
×
613
                elif res == "s":
×
614
                    LOG.info("You can not skip this build.")
×
615
                elif res == "e":
×
616
                    return
×
617
                elif res == "r":
×
618
                    pass
619
                else:
620
                    raise GoodBadExpectationError(
×
621
                        "Build was expected to be %s! The initial good/bad"
622
                        " range seems incorrect." % expected
623
                    )
624

625
        if _evaluate(good, "good"):
×
626
            self.download_manager.focus_download(bad)
×
627
            if self.dl_in_background:
×
628
                # download next build (mid) in background
629
                self.download_manager.download_in_background(
×
630
                    self.build_range[self.build_range.mid_point()]
631
                )
632
            return _evaluate(bad, "bad")
×
633

634
    def handle_verdict(self, mid_point, verdict):
3✔
635
        if verdict == "g":
×
636
            # if build is good and we are looking for a regression, we
637
            # have to split from
638
            # [G, ?, ?, G, ?, B]
639
            # to
640
            #          [G, ?, B]
641
            self.history.add(self.build_range, mid_point, verdict)
×
642
            if not self.handler.find_fix:
×
643
                self.build_range = self.build_range[mid_point:]
×
644
            else:
645
                self.build_range = self.build_range[: mid_point + 1]
×
646
            self.handler.build_good(mid_point, self.build_range)
×
647
        elif verdict == "b":
×
648
            # if build is bad and we are looking for a regression, we
649
            # have to split from
650
            # [G, ?, ?, B, ?, B]
651
            # to
652
            # [G, ?, ?, B]
653
            self.history.add(self.build_range, mid_point, verdict)
×
654
            if not self.handler.find_fix:
×
655
                self.build_range = self.build_range[: mid_point + 1]
×
656
            else:
657
                self.build_range = self.build_range[mid_point:]
×
658
            self.handler.build_bad(mid_point, self.build_range)
×
659
        elif verdict == "r":
×
660
            self.handler.build_retry(mid_point)
×
661
        elif verdict == "s":
×
662
            self.handler.build_skip(mid_point)
×
663
            self.history.add(self.build_range, mid_point, verdict)
×
664
            self.build_range = self.build_range.deleted(mid_point)
×
665
        elif verdict == "back":
×
666
            self.build_range = self.history[-1].build_range
×
667
        else:
668
            # user exit
669
            self.handler.user_exit(mid_point)
×
670
            return self.USER_EXIT
×
671
        return self.RUNNING
×
672

673

674
class Bisector(object):
3✔
675
    """
676
    Handle the logic of the bisection process, and report events to a given
677
    :class:`BisectorHandler`.
678
    """
679

680
    def __init__(
3✔
681
        self,
682
        fetch_config,
683
        test_runner,
684
        download_manager,
685
        dl_in_background=True,
686
        approx_chooser=None,
687
    ):
688
        self.fetch_config = fetch_config
×
689
        self.test_runner = test_runner
×
690
        self.download_manager = download_manager
×
691
        self.dl_in_background = dl_in_background
×
692
        self.approx_chooser = approx_chooser
×
693

694
    def bisect(self, handler, good, bad, **kwargs):
3✔
695
        if handler.find_fix:
×
696
            good, bad = bad, good
×
697
        build_range = handler.create_range(self.fetch_config, good, bad, **kwargs)
×
698

699
        return self._bisect(handler, build_range)
×
700

701
    def _bisect(self, handler, build_range):
3✔
702
        """
703
        Starts a bisection for a :class:`mozregression.build_range.BuildData`.
704
        """
705

706
        bisection = Bisection(
×
707
            handler,
708
            build_range,
709
            self.download_manager,
710
            self.test_runner,
711
            dl_in_background=self.dl_in_background,
712
            approx_chooser=self.approx_chooser,
713
        )
714

715
        previous_verdict = None
×
716

717
        while True:
718
            index = bisection.search_mid_point()
×
719
            result = bisection.init_handler(index)
×
720
            if result != bisection.RUNNING:
×
721
                return result
×
722
            if previous_verdict is None and handler.ensure_good_and_bad:
×
723
                if bisection.ensure_good_and_bad():
×
724
                    LOG.info("Good and bad builds are correct. Let's" " continue the bisection.")
×
725
                else:
726
                    return bisection.USER_EXIT
×
727
            bisection.handler.print_range(full=False)
×
728

729
            if previous_verdict == "back":
×
730
                index = bisection.history.pop(-1).index
×
731

732
            allow_bg_download = True
×
733
            if previous_verdict == "s":
×
734
                # disallow background download since we are not sure of what
735
                # to download next.
736
                allow_bg_download = False
×
737
                index = self.test_runner.index_to_try_after_skip(bisection.build_range)
×
738

739
            index_promise = None
×
740
            build_info = bisection.build_range[index]
×
741
            try:
×
742
                if previous_verdict != "r" and build_info:
×
743
                    # if the last verdict was retry, do not download
744
                    # the build. Futhermore trying to download if we are
745
                    # in background download mode would stop the next builds
746
                    # from downloading.
747
                    index_promise, build_info = bisection.download_build(
×
748
                        index, allow_bg_download=allow_bg_download
749
                    )
750

751
                if not build_info:
×
752
                    LOG.info("Unable to find build info. Skipping this build...")
×
753
                    verdict = "s"
×
754
                else:
755
                    try:
×
756
                        verdict = bisection.evaluate(build_info)
×
757
                    except LauncherError as exc:
×
758
                        # we got an unrecoverable error while trying
759
                        # to run the tested app. We can just fallback
760
                        # to skip the build.
761
                        LOG.info("Error: %s. Skipping this build..." % exc)
×
762
                        verdict = "s"
×
763
            finally:
764
                # be sure to terminate the index_promise thread in all
765
                # circumstances.
766
                if index_promise:
×
767
                    index = index_promise()
×
768
            previous_verdict = verdict
×
769
            result = bisection.handle_verdict(index, verdict)
×
770
            if result != bisection.RUNNING:
×
771
                return result
×
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