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

mozilla / mozregression / 12352640211

16 Dec 2024 12:28PM CUT coverage: 89.405%. First build
12352640211

Pull #1906

github

web-flow
Merge ea68a5bf9 into 807564865
Pull Request #1906: build(deps): bump taskcluster from 75.0.0 to 76.0.0

2523 of 2822 relevant lines covered (89.4%)

13.53 hits per line

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

81.31
/mozregression/bisector.py
1
from __future__ import absolute_import
17✔
2

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

8
from mozlog import get_proxy_logger
17✔
9

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

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

24

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

30

31
class BisectorHandler(metaclass=ABCMeta):
17✔
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):
17✔
39
        self.find_fix = find_fix
14✔
40
        self.ensure_good_and_bad = ensure_good_and_bad
14✔
41
        self.found_repo = None
14✔
42
        self.build_range = None
14✔
43
        self.good_revision = None
14✔
44
        self.bad_revision = None
14✔
45

46
    def set_build_range(self, build_range):
17✔
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
14✔
55

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

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

66
    def initialize(self):
17✔
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
14✔
76
        if repo is not None:
14✔
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
14✔
80
        self.good_revision, self.bad_revision = self._reverse_if_find_fix(
14✔
81
            self.build_range[0].changeset, self.build_range[-1].changeset
82
        )
83

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

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

97
    def print_range(self, good_date=None, bad_date=None, full=True):
17✔
98
        """
99
        Log the state of the current state of the bisection process, with an
100
        appropriate pushlog url.
101
        """
102
        if full:
14✔
103
            if good_date and bad_date:
14✔
104
                good_date = " (%s)" % good_date
14✔
105
                bad_date = " (%s)" % bad_date
14✔
106
            words = self._reverse_if_find_fix("Last", "First")
14✔
107
            LOG.info(
14✔
108
                "%s good revision: %s%s"
109
                % (words[0], self.good_revision, good_date if good_date else "")
110
            )
111
            LOG.info(
14✔
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())
14✔
116

117
    def build_good(self, mid, new_data):
17✔
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)
14✔
124

125
    def build_bad(self, mid, new_data):
17✔
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)
14✔
132

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

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

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

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

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

148

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

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

161
    def _print_progress(self, new_data):
17✔
162
        good_date, bad_date = self._reverse_if_find_fix(self.good_date, self.bad_date)
14✔
163
        next_good_date = new_data[0].build_date
14✔
164
        next_bad_date = new_data[-1].build_date
14✔
165
        next_days_range = abs((to_datetime(next_bad_date) - to_datetime(next_good_date)).days)
14✔
166
        LOG.info(
14✔
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):
17✔
183
        words = self._reverse_if_find_fix("Newest", "Oldest")
14✔
184
        LOG.info("%s known good nightly: %s" % (words[0], self.good_date))
14✔
185
        LOG.info("%s known bad nightly: %s" % (words[1], self.bad_date))
14✔
186

187
    def user_exit(self, mid):
17✔
188
        self._print_date_range()
14✔
189

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

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

196
    def print_range(self, full=True):
17✔
197
        if self.found_repo is None:
14✔
198
            # this may happen if we are bisecting old builds without
199
            # enough tests of the builds.
200
            LOG.error(
14✔
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:
14✔
206
                self._print_date_range()
14✔
207
        elif self.are_revisions_available():
14✔
208
            BisectorHandler.print_range(self, self.good_date, self.bad_date, full=full)
14✔
209
        else:
210
            if full:
14✔
211
                self._print_date_range()
14✔
212
            LOG.info("Pushlog:\n%s\n" % self.get_pushlog_url())
14✔
213

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

226

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

230
    def _print_progress(self, new_data):
17✔
231
        LOG.info(
14✔
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):
17✔
248
        words = self._reverse_if_find_fix("Newest", "Oldest")
14✔
249
        LOG.info("%s known good integration revision: %s" % (words[0], self.good_revision))
14✔
250
        LOG.info("%s known bad integration revision: %s" % (words[1], self.bad_revision))
14✔
251

252
    def _choose_integration_branch(self, changeset):
17✔
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):
17✔
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 IndexPromise(object):
17✔
355
    """
356
    A promise to get a build index.
357

358
    Provide a callable object that gives the next index when called.
359
    """
360

361
    def __init__(self, index, callback=None, args=()):
17✔
362
        self.thread = None
14✔
363
        self.index = index
14✔
364
        if callback:
14✔
365
            self.thread = threading.Thread(target=self._run, args=(callback,) + args)
14✔
366
            self.thread.start()
14✔
367

368
    def _run(self, callback, *args):
17✔
369
        self.index = callback(self.index, *args)
14✔
370

371
    def __call__(self):
17✔
372
        if self.thread:
14✔
373
            self.thread.join()
14✔
374
        return self.index
14✔
375

376

377
class Bisection(object):
17✔
378
    RUNNING = 0
17✔
379
    NO_DATA = 1
17✔
380
    FINISHED = 2
17✔
381
    USER_EXIT = 3
17✔
382

383
    def __init__(
17✔
384
        self,
385
        handler,
386
        build_range,
387
        download_manager,
388
        test_runner,
389
        dl_in_background=True,
390
        approx_chooser=None,
391
    ):
392
        self.handler = handler
14✔
393
        self.build_range = build_range
14✔
394
        self.download_manager = download_manager
14✔
395
        self.test_runner = test_runner
14✔
396
        self.dl_in_background = dl_in_background
14✔
397
        self.history = BisectionHistory()
14✔
398
        self.approx_chooser = approx_chooser
14✔
399

400
    def search_mid_point(self, interrupt=None):
17✔
401
        self.handler.set_build_range(self.build_range)
14✔
402
        return self._search_mid_point(interrupt=interrupt)
14✔
403

404
    def _search_mid_point(self, interrupt=None):
17✔
405
        return self.build_range.mid_point(interrupt=interrupt)
14✔
406

407
    def init_handler(self, mid_point):
17✔
408
        if len(self.build_range) == 0:
14✔
409
            self.handler.no_data()
14✔
410
            return self.NO_DATA
14✔
411

412
        self.handler.initialize()
14✔
413

414
        if mid_point == 0:
14✔
415
            self.handler.finished()
14✔
416
            return self.FINISHED
14✔
417
        return self.RUNNING
14✔
418

419
    def download_build(self, mid_point, allow_bg_download=True):
17✔
420
        """
421
        Download the build for the given mid_point.
422

423
        This call may start the download of next builds in background (if
424
        dl_in_background evaluates to True). Note that the mid point may
425
        change in this case.
426

427
        Returns a couple (index_promise, build_infos) where build_infos
428
        is the dict of build infos for the build.
429
        """
430
        build_infos = self.handler.build_range[mid_point]
14✔
431
        return self._download_build(mid_point, build_infos, allow_bg_download=allow_bg_download)
14✔
432

433
    def _find_approx_build(self, mid_point, build_infos):
17✔
434
        approx_index, persist_files = None, ()
14✔
435
        if self.approx_chooser:
14✔
436
            # try to find an approx build
437
            persist_files = os.listdir(self.download_manager.destdir)
×
438
            # first test if we have the exact file - if we do,
439
            # just act as usual, the downloader will take care of it.
440
            if build_infos.persist_filename not in persist_files:
×
441
                approx_index = self.approx_chooser.index(
×
442
                    self.build_range, build_infos, persist_files
443
                )
444
        if approx_index is not None:
14✔
445
            # we found an approx build. First, stop possible background
446
            # downloads, then update the mid point and build info.
447
            if self.download_manager.background_dl_policy == "cancel":
×
448
                self.download_manager.cancel()
×
449

450
            old_url = build_infos.build_url
×
451
            mid_point = approx_index
×
452
            build_infos = self.build_range[approx_index]
×
453
            fname = self.download_manager.get_dest(build_infos.persist_filename)
×
454
            LOG.info(
×
455
                "Using `%s` as an acceptable approximated"
456
                " build file instead of downloading %s" % (fname, old_url)
457
            )
458
            build_infos.build_file = fname
×
459
        return (approx_index is not None, mid_point, build_infos, persist_files)
14✔
460

461
    def _download_build(self, mid_point, build_infos, allow_bg_download=True):
17✔
462
        found, mid_point, build_infos, persist_files = self._find_approx_build(
14✔
463
            mid_point, build_infos
464
        )
465
        if not found and self.download_manager:
14✔
466
            # else, do the download. Note that nothing will
467
            # be downloaded if the exact build file is already present.
468
            self.download_manager.focus_download(build_infos)
14✔
469
        callback = None
14✔
470
        if self.dl_in_background and allow_bg_download:
14✔
471
            callback = self._download_next_builds
14✔
472
        return (IndexPromise(mid_point, callback, args=(persist_files,)), build_infos)
14✔
473

474
    def _download_next_builds(self, mid_point, persist_files=()):
17✔
475
        # start downloading the next builds.
476
        # note that we don't have to worry if builds are already
477
        # downloaded, or if our build infos are the same because
478
        # this will be handled by the downloadmanager.
479
        def start_dl(r):
14✔
480
            # first get the next mid point
481
            # this will trigger some blocking downloads
482
            # (we need to find the build info)
483
            m = r.mid_point()
14✔
484
            if len(r) != 0:
14✔
485
                # non-blocking download of the build
486
                if (
14✔
487
                    self.approx_chooser
488
                    and self.approx_chooser.index(r, r[m], persist_files) is not None
489
                ):
490
                    pass  # nothing to download, we have an approx build
491
                else:
492
                    self.download_manager.download_in_background(r[m])
14✔
493

494
        bdata = self.build_range[mid_point]
14✔
495
        # download next left mid point
496
        start_dl(self.build_range[mid_point:])
14✔
497
        # download right next mid point
498
        start_dl(self.build_range[: mid_point + 1])
14✔
499
        # since we called mid_point() on copy of self.build_range instance,
500
        # the underlying cache may have changed and we need to find the new
501
        # mid point.
502
        self.build_range.filter_invalid_builds()
14✔
503
        return self.build_range.index(bdata)
14✔
504

505
    def evaluate(self, build_infos):
17✔
506
        verdict = self.test_runner.evaluate(build_infos, allow_back=bool(self.history))
14✔
507
        # old builds do not have metadata about the repo. But once
508
        # the build is installed, we may have it
509
        if self.handler.found_repo is None:
14✔
510
            self.handler.found_repo = build_infos.repo_url
×
511
        return verdict
14✔
512

513
    def ensure_good_and_bad(self):
17✔
514
        good, bad = self.build_range[0], self.build_range[-1]
14✔
515
        if self.handler.find_fix:
14✔
516
            good, bad = bad, good
14✔
517

518
        LOG.info("Testing good and bad builds to ensure that they are" " really good and bad...")
14✔
519
        self.download_manager.focus_download(good)
14✔
520
        if self.dl_in_background:
14✔
521
            self.download_manager.download_in_background(bad)
14✔
522

523
        def _evaluate(build_info, expected):
14✔
524
            while 1:
9✔
525
                res = self.test_runner.evaluate(build_info)
14✔
526
                if res == expected[0]:
14✔
527
                    return True
14✔
528
                elif res == "s":
14✔
529
                    LOG.info("You can not skip this build.")
14✔
530
                elif res == "e":
14✔
531
                    return
14✔
532
                elif res == "r":
14✔
533
                    pass
534
                else:
535
                    raise GoodBadExpectationError(
14✔
536
                        "Build was expected to be %s! The initial good/bad"
537
                        " range seems incorrect." % expected
538
                    )
539

540
        if _evaluate(good, "good"):
14✔
541
            self.download_manager.focus_download(bad)
14✔
542
            if self.dl_in_background:
14✔
543
                # download next build (mid) in background
544
                self.download_manager.download_in_background(
14✔
545
                    self.build_range[self.build_range.mid_point()]
546
                )
547
            return _evaluate(bad, "bad")
14✔
548

549
    def handle_verdict(self, mid_point, verdict):
17✔
550
        if verdict == "g":
14✔
551
            # if build is good and we are looking for a regression, we
552
            # have to split from
553
            # [G, ?, ?, G, ?, B]
554
            # to
555
            #          [G, ?, B]
556
            self.history.add(self.build_range, mid_point, verdict)
14✔
557
            if not self.handler.find_fix:
14✔
558
                self.build_range = self.build_range[mid_point:]
14✔
559
            else:
560
                self.build_range = self.build_range[: mid_point + 1]
14✔
561
            self.handler.build_good(mid_point, self.build_range)
14✔
562
        elif verdict == "b":
14✔
563
            # if build is bad and we are looking for a regression, we
564
            # have to split from
565
            # [G, ?, ?, B, ?, B]
566
            # to
567
            # [G, ?, ?, B]
568
            self.history.add(self.build_range, mid_point, verdict)
14✔
569
            if not self.handler.find_fix:
14✔
570
                self.build_range = self.build_range[: mid_point + 1]
14✔
571
            else:
572
                self.build_range = self.build_range[mid_point:]
14✔
573
            self.handler.build_bad(mid_point, self.build_range)
14✔
574
        elif verdict == "r":
14✔
575
            self.handler.build_retry(mid_point)
14✔
576
        elif verdict == "s":
14✔
577
            self.handler.build_skip(mid_point)
14✔
578
            self.history.add(self.build_range, mid_point, verdict)
14✔
579
            self.build_range = self.build_range.deleted(mid_point)
14✔
580
        elif verdict == "back":
14✔
581
            self.build_range = self.history[-1].build_range
14✔
582
        else:
583
            # user exit
584
            self.handler.user_exit(mid_point)
14✔
585
            return self.USER_EXIT
14✔
586
        return self.RUNNING
14✔
587

588

589
class Bisector(object):
17✔
590
    """
591
    Handle the logic of the bisection process, and report events to a given
592
    :class:`BisectorHandler`.
593
    """
594

595
    def __init__(
17✔
596
        self,
597
        fetch_config,
598
        test_runner,
599
        download_manager,
600
        dl_in_background=True,
601
        approx_chooser=None,
602
    ):
603
        self.fetch_config = fetch_config
14✔
604
        self.test_runner = test_runner
14✔
605
        self.download_manager = download_manager
14✔
606
        self.dl_in_background = dl_in_background
14✔
607
        self.approx_chooser = approx_chooser
14✔
608

609
    def bisect(self, handler, good, bad, **kwargs):
17✔
610
        if handler.find_fix:
14✔
611
            good, bad = bad, good
14✔
612
        build_range = handler.create_range(self.fetch_config, good, bad, **kwargs)
14✔
613

614
        return self._bisect(handler, build_range)
14✔
615

616
    def _bisect(self, handler, build_range):
17✔
617
        """
618
        Starts a bisection for a :class:`mozregression.build_range.BuildData`.
619
        """
620

621
        bisection = Bisection(
14✔
622
            handler,
623
            build_range,
624
            self.download_manager,
625
            self.test_runner,
626
            dl_in_background=self.dl_in_background,
627
            approx_chooser=self.approx_chooser,
628
        )
629

630
        previous_verdict = None
14✔
631

632
        while True:
9✔
633
            index = bisection.search_mid_point()
14✔
634
            result = bisection.init_handler(index)
14✔
635
            if result != bisection.RUNNING:
14✔
636
                return result
14✔
637
            if previous_verdict is None and handler.ensure_good_and_bad:
14✔
638
                if bisection.ensure_good_and_bad():
14✔
639
                    LOG.info("Good and bad builds are correct. Let's" " continue the bisection.")
14✔
640
                else:
641
                    return bisection.USER_EXIT
14✔
642
            bisection.handler.print_range(full=False)
14✔
643

644
            if previous_verdict == "back":
14✔
645
                index = bisection.history.pop(-1).index
14✔
646

647
            allow_bg_download = True
14✔
648
            if previous_verdict == "s":
14✔
649
                # disallow background download since we are not sure of what
650
                # to download next.
651
                allow_bg_download = False
×
652
                index = self.test_runner.index_to_try_after_skip(bisection.build_range)
×
653

654
            index_promise = None
14✔
655
            build_info = bisection.build_range[index]
14✔
656
            try:
14✔
657
                if previous_verdict != "r" and build_info:
14✔
658
                    # if the last verdict was retry, do not download
659
                    # the build. Futhermore trying to download if we are
660
                    # in background download mode would stop the next builds
661
                    # from downloading.
662
                    index_promise, build_info = bisection.download_build(
14✔
663
                        index, allow_bg_download=allow_bg_download
664
                    )
665

666
                if not build_info:
14✔
667
                    LOG.info("Unable to find build info. Skipping this build...")
×
668
                    verdict = "s"
×
669
                else:
670
                    try:
14✔
671
                        verdict = bisection.evaluate(build_info)
14✔
672
                    except LauncherError as exc:
14✔
673
                        # we got an unrecoverable error while trying
674
                        # to run the tested app. We can just fallback
675
                        # to skip the build.
676
                        LOG.info("Error: %s. Skipping this build..." % exc)
14✔
677
                        verdict = "s"
14✔
678
            finally:
679
                # be sure to terminate the index_promise thread in all
680
                # circumstances.
681
                if index_promise:
14✔
682
                    index = index_promise()
14✔
683
            previous_verdict = verdict
14✔
684
            result = bisection.handle_verdict(index, verdict)
14✔
685
            if result != bisection.RUNNING:
14✔
686
                return result
14✔
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