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

mozilla / mozregression / 11572580249

29 Oct 2024 11:06AM CUT coverage: 35.11%. First build
11572580249

Pull #1450

github

web-flow
Merge 25ca7f46b 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%)

1054 of 3002 relevant lines covered (35.11%)

1.05 hits per line

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

22.8
/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
"""
1✔
355
We are just using this to make it clear we have no merge to take care of
356
We are running an Integration because builds are triggered from cron jobs
357
on mozilla-central for all Snap package branches
358
"""
359

360

361
class SnapHandler(IntegrationHandler):
3✔
362
    snap_repo = None
3✔
363
    _build_infos = {}
3✔
364
    snap_rev = {}
3✔
365

366
    def record_build_infos(self, build_infos):
3✔
NEW
367
        self._build_infos["_changeset"] = build_infos._changeset
×
NEW
368
        self._build_infos["_repo_url"] = build_infos._repo_url
×
NEW
369
        self.snap_repo = build_infos._repo_url
×
370

371
    def update_build_infos(self, build_infos):
3✔
372
        # _build_infos here holds the mozilla-central ones,
373
        # build_infos should be the snap-specific one
NEW
374
        self.snap_rev[self._build_infos["_changeset"]] = build_infos.changeset
×
NEW
375
        self.snap_repo = build_infos._repo_url
×
376

377
    def get_pushlog_url(self):
3✔
378
        # somehow, self.found_repo from this class would not reflect
NEW
379
        first_rev, last_rev = self.get_range()
×
NEW
380
        if first_rev == last_rev:
×
NEW
381
            return "%s/pushloghtml?changeset=%s" % (self.snap_repo, first_rev)
×
NEW
382
        return "%s/pushloghtml?fromchange=%s&tochange=%s" % (
×
383
            self.snap_repo,
384
            first_rev,
385
            last_rev,
386
        )
387

388
    def revert_build_infos(self, build_infos):
3✔
NEW
389
        build_infos._changeset = self._build_infos["_changeset"]
×
NEW
390
        build_infos._repo_url = self._build_infos["_repo_url"]
×
391

392
    def handle_merge(self):
3✔
NEW
393
        return None
×
394

395

396
class IndexPromise(object):
3✔
397
    """
398
    A promise to get a build index.
399

400
    Provide a callable object that gives the next index when called.
401
    """
402

403
    def __init__(self, index, callback=None, args=()):
3✔
404
        self.thread = None
×
405
        self.index = index
×
406
        if callback:
×
407
            self.thread = threading.Thread(target=self._run, args=(callback,) + args)
×
408
            self.thread.start()
×
409

410
    def _run(self, callback, *args):
3✔
411
        self.index = callback(self.index, *args)
×
412

413
    def __call__(self):
3✔
414
        if self.thread:
×
415
            self.thread.join()
×
416
        return self.index
×
417

418

419
class Bisection(object):
3✔
420
    RUNNING = 0
3✔
421
    NO_DATA = 1
3✔
422
    FINISHED = 2
3✔
423
    USER_EXIT = 3
3✔
424

425
    def __init__(
3✔
426
        self,
427
        handler,
428
        build_range,
429
        download_manager,
430
        test_runner,
431
        dl_in_background=True,
432
        approx_chooser=None,
433
    ):
434
        self.handler = handler
×
435
        self.build_range = build_range
×
436
        self.download_manager = download_manager
×
437
        self.test_runner = test_runner
×
438
        self.dl_in_background = dl_in_background
×
439
        self.history = BisectionHistory()
×
440
        self.approx_chooser = approx_chooser
×
441

442
    def search_mid_point(self, interrupt=None):
3✔
443
        self.handler.set_build_range(self.build_range)
×
444
        return self._search_mid_point(interrupt=interrupt)
×
445

446
    def _search_mid_point(self, interrupt=None):
3✔
447
        return self.build_range.mid_point(interrupt=interrupt)
×
448

449
    def init_handler(self, mid_point):
3✔
450
        if len(self.build_range) == 0:
×
451
            self.handler.no_data()
×
452
            return self.NO_DATA
×
453

454
        self.handler.initialize()
×
455

456
        if mid_point == 0:
×
457
            self.handler.finished()
×
458
            return self.FINISHED
×
459
        return self.RUNNING
×
460

461
    def download_build(self, mid_point, allow_bg_download=True):
3✔
462
        """
463
        Download the build for the given mid_point.
464

465
        This call may start the download of next builds in background (if
466
        dl_in_background evaluates to True). Note that the mid point may
467
        change in this case.
468

469
        Returns a couple (index_promise, build_infos) where build_infos
470
        is the dict of build infos for the build.
471
        """
472
        build_infos = self.handler.build_range[mid_point]
×
473
        return self._download_build(mid_point, build_infos, allow_bg_download=allow_bg_download)
×
474

475
    def _find_approx_build(self, mid_point, build_infos):
3✔
476
        approx_index, persist_files = None, ()
×
477
        if self.approx_chooser:
×
478
            # try to find an approx build
479
            persist_files = os.listdir(self.download_manager.destdir)
×
480
            # first test if we have the exact file - if we do,
481
            # just act as usual, the downloader will take care of it.
482
            if build_infos.persist_filename not in persist_files:
×
483
                approx_index = self.approx_chooser.index(
×
484
                    self.build_range, build_infos, persist_files
485
                )
486
        if approx_index is not None:
×
487
            # we found an approx build. First, stop possible background
488
            # downloads, then update the mid point and build info.
489
            if self.download_manager.background_dl_policy == "cancel":
×
490
                self.download_manager.cancel()
×
491

492
            old_url = build_infos.build_url
×
493
            mid_point = approx_index
×
494
            build_infos = self.build_range[approx_index]
×
495
            fname = self.download_manager.get_dest(build_infos.persist_filename)
×
496
            LOG.info(
×
497
                "Using `%s` as an acceptable approximated"
498
                " build file instead of downloading %s" % (fname, old_url)
499
            )
500
            build_infos.build_file = fname
×
501
        return (approx_index is not None, mid_point, build_infos, persist_files)
×
502

503
    def _download_build(self, mid_point, build_infos, allow_bg_download=True):
3✔
504
        found, mid_point, build_infos, persist_files = self._find_approx_build(
×
505
            mid_point, build_infos
506
        )
507
        if not found and self.download_manager:
×
508
            # else, do the download. Note that nothing will
509
            # be downloaded if the exact build file is already present.
510
            self.download_manager.focus_download(build_infos)
×
511
        callback = None
×
512
        if self.dl_in_background and allow_bg_download:
×
513
            callback = self._download_next_builds
×
514
        return (IndexPromise(mid_point, callback, args=(persist_files,)), build_infos)
×
515

516
    def _download_next_builds(self, mid_point, persist_files=()):
3✔
517
        # start downloading the next builds.
518
        # note that we don't have to worry if builds are already
519
        # downloaded, or if our build infos are the same because
520
        # this will be handled by the downloadmanager.
521
        def start_dl(r):
×
522
            # first get the next mid point
523
            # this will trigger some blocking downloads
524
            # (we need to find the build info)
525
            m = r.mid_point()
×
526
            if len(r) != 0:
×
527
                # non-blocking download of the build
528
                if (
×
529
                    self.approx_chooser
530
                    and self.approx_chooser.index(r, r[m], persist_files) is not None
531
                ):
532
                    pass  # nothing to download, we have an approx build
533
                else:
534
                    self.download_manager.download_in_background(r[m])
×
535

536
        bdata = self.build_range[mid_point]
×
537
        # download next left mid point
538
        start_dl(self.build_range[mid_point:])
×
539
        # download right next mid point
540
        start_dl(self.build_range[: mid_point + 1])
×
541
        # since we called mid_point() on copy of self.build_range instance,
542
        # the underlying cache may have changed and we need to find the new
543
        # mid point.
544
        self.build_range.filter_invalid_builds()
×
545
        return self.build_range.index(bdata)
×
546

547
    def evaluate(self, build_infos):
3✔
548
        # we force getting data from app info for snap since we are building everything
549
        # out of mozilla-central
NEW
550
        if isinstance(self.handler, SnapHandler):
×
NEW
551
            self.handler.record_build_infos(build_infos)
×
552
        verdict = self.test_runner.evaluate(build_infos, allow_back=bool(self.history))
×
553
        # old builds do not have metadata about the repo. But once
554
        # the build is installed, we may have it
555
        if self.handler.found_repo is None:
×
556
            self.handler.found_repo = build_infos.repo_url
×
NEW
557
        if isinstance(self.handler, SnapHandler):
×
558
            # Some Snap nightly builds are missing SourceRepository/SourceStamp
559
            # So since we dont have a better source of information, let's get back
560
            # what we had
NEW
561
            if build_infos.repo_url is None:
×
NEW
562
                LOG.warning(
×
563
                    "Bisection on a Snap package missing SourceRepository/SourceStamp,"
564
                    " falling back to mozilla-central revs."
565
                )
NEW
566
                self.handler.revert_build_infos(build_infos)
×
567
            else:
NEW
568
                self.handler.update_build_infos(build_infos)
×
569
        return verdict
×
570

571
    def ensure_good_and_bad(self):
3✔
572
        good, bad = self.build_range[0], self.build_range[-1]
×
573
        if self.handler.find_fix:
×
574
            good, bad = bad, good
×
575

576
        LOG.info("Testing good and bad builds to ensure that they are" " really good and bad...")
×
577
        self.download_manager.focus_download(good)
×
578
        if self.dl_in_background:
×
579
            self.download_manager.download_in_background(bad)
×
580

581
        def _evaluate(build_info, expected):
×
582
            while 1:
583
                res = self.test_runner.evaluate(build_info)
×
584
                if res == expected[0]:
×
585
                    return True
×
586
                elif res == "s":
×
587
                    LOG.info("You can not skip this build.")
×
588
                elif res == "e":
×
589
                    return
×
590
                elif res == "r":
×
591
                    pass
592
                else:
593
                    raise GoodBadExpectationError(
×
594
                        "Build was expected to be %s! The initial good/bad"
595
                        " range seems incorrect." % expected
596
                    )
597

598
        if _evaluate(good, "good"):
×
599
            self.download_manager.focus_download(bad)
×
600
            if self.dl_in_background:
×
601
                # download next build (mid) in background
602
                self.download_manager.download_in_background(
×
603
                    self.build_range[self.build_range.mid_point()]
604
                )
605
            return _evaluate(bad, "bad")
×
606

607
    def handle_verdict(self, mid_point, verdict):
3✔
608
        if verdict == "g":
×
609
            # if build is good and we are looking for a regression, we
610
            # have to split from
611
            # [G, ?, ?, G, ?, B]
612
            # to
613
            #          [G, ?, B]
614
            self.history.add(self.build_range, mid_point, verdict)
×
615
            if not self.handler.find_fix:
×
616
                self.build_range = self.build_range[mid_point:]
×
617
            else:
618
                self.build_range = self.build_range[: mid_point + 1]
×
619
            self.handler.build_good(mid_point, self.build_range)
×
620
        elif verdict == "b":
×
621
            # if build is bad and we are looking for a regression, we
622
            # have to split from
623
            # [G, ?, ?, B, ?, B]
624
            # to
625
            # [G, ?, ?, B]
626
            self.history.add(self.build_range, mid_point, verdict)
×
627
            if not self.handler.find_fix:
×
628
                self.build_range = self.build_range[: mid_point + 1]
×
629
            else:
630
                self.build_range = self.build_range[mid_point:]
×
631
            self.handler.build_bad(mid_point, self.build_range)
×
632
        elif verdict == "r":
×
633
            self.handler.build_retry(mid_point)
×
634
        elif verdict == "s":
×
635
            self.handler.build_skip(mid_point)
×
636
            self.history.add(self.build_range, mid_point, verdict)
×
637
            self.build_range = self.build_range.deleted(mid_point)
×
638
        elif verdict == "back":
×
639
            self.build_range = self.history[-1].build_range
×
640
        else:
641
            # user exit
642
            self.handler.user_exit(mid_point)
×
643
            return self.USER_EXIT
×
644
        return self.RUNNING
×
645

646

647
class Bisector(object):
3✔
648
    """
649
    Handle the logic of the bisection process, and report events to a given
650
    :class:`BisectorHandler`.
651
    """
652

653
    def __init__(
3✔
654
        self,
655
        fetch_config,
656
        test_runner,
657
        download_manager,
658
        dl_in_background=True,
659
        approx_chooser=None,
660
    ):
661
        self.fetch_config = fetch_config
×
662
        self.test_runner = test_runner
×
663
        self.download_manager = download_manager
×
664
        self.dl_in_background = dl_in_background
×
665
        self.approx_chooser = approx_chooser
×
666

667
    def bisect(self, handler, good, bad, **kwargs):
3✔
668
        if handler.find_fix:
×
669
            good, bad = bad, good
×
670
        build_range = handler.create_range(self.fetch_config, good, bad, **kwargs)
×
671

672
        return self._bisect(handler, build_range)
×
673

674
    def _bisect(self, handler, build_range):
3✔
675
        """
676
        Starts a bisection for a :class:`mozregression.build_range.BuildData`.
677
        """
678

679
        bisection = Bisection(
×
680
            handler,
681
            build_range,
682
            self.download_manager,
683
            self.test_runner,
684
            dl_in_background=self.dl_in_background,
685
            approx_chooser=self.approx_chooser,
686
        )
687

688
        previous_verdict = None
×
689

690
        while True:
691
            index = bisection.search_mid_point()
×
692
            result = bisection.init_handler(index)
×
693
            if result != bisection.RUNNING:
×
694
                return result
×
695
            if previous_verdict is None and handler.ensure_good_and_bad:
×
696
                if bisection.ensure_good_and_bad():
×
697
                    LOG.info("Good and bad builds are correct. Let's" " continue the bisection.")
×
698
                else:
699
                    return bisection.USER_EXIT
×
700
            bisection.handler.print_range(full=False)
×
701

702
            if previous_verdict == "back":
×
703
                index = bisection.history.pop(-1).index
×
704

705
            allow_bg_download = True
×
706
            if previous_verdict == "s":
×
707
                # disallow background download since we are not sure of what
708
                # to download next.
709
                allow_bg_download = False
×
710
                index = self.test_runner.index_to_try_after_skip(bisection.build_range)
×
711

712
            index_promise = None
×
713
            build_info = bisection.build_range[index]
×
714
            try:
×
715
                if previous_verdict != "r" and build_info:
×
716
                    # if the last verdict was retry, do not download
717
                    # the build. Futhermore trying to download if we are
718
                    # in background download mode would stop the next builds
719
                    # from downloading.
720
                    index_promise, build_info = bisection.download_build(
×
721
                        index, allow_bg_download=allow_bg_download
722
                    )
723

724
                if not build_info:
×
725
                    LOG.info("Unable to find build info. Skipping this build...")
×
726
                    verdict = "s"
×
727
                else:
728
                    try:
×
729
                        verdict = bisection.evaluate(build_info)
×
730
                    except LauncherError as exc:
×
731
                        # we got an unrecoverable error while trying
732
                        # to run the tested app. We can just fallback
733
                        # to skip the build.
734
                        LOG.info("Error: %s. Skipping this build..." % exc)
×
735
                        verdict = "s"
×
736
            finally:
737
                # be sure to terminate the index_promise thread in all
738
                # circumstances.
739
                if index_promise:
×
740
                    index = index_promise()
×
741
            previous_verdict = verdict
×
742
            result = bisection.handle_verdict(index, verdict)
×
743
            if result != bisection.RUNNING:
×
744
                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