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

mozilla / mozregression / 9283882049

29 May 2024 09:44AM CUT coverage: 86.286%. First build
9283882049

Pull #1450

github

web-flow
Merge ee901c877 into 3e46da41f
Pull Request #1450: Bug 1763188 - Add Snap support using TC builds

96 of 217 new or added lines in 8 files covered. (44.24%)

2586 of 2997 relevant lines covered (86.29%)

5.86 hits per line

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

89.23
/mozregression/main.py
1
"""
2
Entry point for the mozregression command line.
3
"""
4

5
from __future__ import absolute_import
6✔
6

7
import atexit
6✔
8
import os
6✔
9
import pipes
6✔
10
import sys
6✔
11

12
import colorama
6✔
13
import mozfile
6✔
14
import requests
6✔
15
from mozlog import get_proxy_logger
6✔
16
from requests.exceptions import HTTPError, RequestException
6✔
17

18
from mozregression import __version__
6✔
19
from mozregression.approx_persist import ApproxPersistChooser
6✔
20
from mozregression.bisector import (
6✔
21
    Bisection,
22
    Bisector,
23
    IntegrationHandler,
24
    NightlyHandler,
25
    SnapHandler,
26
)
27
from mozregression.bugzilla import bug_url, find_bugids_in_push
6✔
28
from mozregression.cli import cli
6✔
29
from mozregression.config import DEFAULT_EXPAND, TC_CREDENTIALS_FNAME
6✔
30
from mozregression.download_manager import BuildDownloadManager
6✔
31
from mozregression.errors import GoodBadExpectationError, MozRegressionError
6✔
32
from mozregression.fetch_build_info import IntegrationInfoFetcher, NightlyInfoFetcher
6✔
33
from mozregression.json_pushes import JsonPushes
6✔
34
from mozregression.launchers import REGISTRY as APP_REGISTRY
6✔
35
from mozregression.network import set_http_session
6✔
36
from mozregression.persist_limit import PersistLimit
6✔
37
from mozregression.telemetry import UsageMetrics, get_system_info, send_telemetry_ping_oop
6✔
38
from mozregression.tempdir import safe_mkdtemp
6✔
39
from mozregression.test_runner import CommandTestRunner, ManualTestRunner
6✔
40

41
LOG = get_proxy_logger("main")
6✔
42

43

44
class Application(object):
6✔
45
    def __init__(self, fetch_config, options):
6✔
46
        self.fetch_config = fetch_config
6✔
47
        self.options = options
6✔
48
        self._test_runner = None
6✔
49
        self._bisector = None
6✔
50
        self._build_download_manager = None
6✔
51
        self._download_dir = options.persist
6✔
52
        self._rm_download_dir = False
6✔
53
        if not options.persist:
6✔
54
            self._download_dir = safe_mkdtemp()
6✔
55
            self._rm_download_dir = True
6✔
56
        launcher_class = APP_REGISTRY.get(fetch_config.app_name)
6✔
57
        launcher_class.check_is_runnable()
6✔
58
        # init global profile if required
59
        self._global_profile = None
6✔
60
        if options.profile_persistence in ("clone-first", "reuse"):
6✔
61
            self._global_profile = launcher_class.create_profile(
×
62
                profile=options.profile,
63
                addons=options.addons,
64
                preferences=options.preferences,
65
                clone=options.profile_persistence == "clone-first",
66
            )
67
            options.cmdargs = options.cmdargs + ["--allow-downgrade"]
×
68
        elif options.profile:
6✔
69
            options.cmdargs = options.cmdargs + ["--allow-downgrade"]
6✔
70

71
    def clear(self):
6✔
72
        if self._build_download_manager:
6✔
73
            # cancel all possible downloads
74
            self._build_download_manager.cancel()
6✔
75
        if self._rm_download_dir:
6✔
76
            if self._build_download_manager:
6✔
77
                # we need to wait explicitly for downloading threads completion
78
                # here because it may remove a file in the download dir - and
79
                # in that case we could end up with a race condition when
80
                # we will remove the download dir. See
81
                # https://bugzilla.mozilla.org/show_bug.cgi?id=1231745
82
                self._build_download_manager.wait(raise_if_error=False)
6✔
83
            mozfile.remove(self._download_dir)
6✔
84
        if self._global_profile and self.options.profile_persistence == "clone-first":
6✔
85
            self._global_profile.cleanup()
×
86

87
    @property
6✔
88
    def test_runner(self):
6✔
89
        if self._test_runner is None:
6✔
90
            if self.options.command is None:
6✔
91
                self._test_runner = ManualTestRunner(
6✔
92
                    launcher_kwargs=dict(
93
                        addons=self.options.addons,
94
                        profile=self._global_profile or self.options.profile,
95
                        cmdargs=self.options.cmdargs,
96
                        preferences=self.options.preferences,
97
                        adb_profile_dir=self.options.adb_profile_dir,
98
                        allow_sudo=self.options.allow_sudo,
99
                        disable_snap_connect=self.options.disable_snap_connect,
100
                    )
101
                )
102
            else:
103
                self._test_runner = CommandTestRunner(self.options.command)
6✔
104
        return self._test_runner
6✔
105

106
    @property
6✔
107
    def bisector(self):
6✔
108
        if self._bisector is None:
6✔
109
            self._bisector = Bisector(
6✔
110
                self.fetch_config,
111
                self.test_runner,
112
                self.build_download_manager,
113
                dl_in_background=self.options.background_dl,
114
                approx_chooser=(
115
                    None if self.options.approx_policy != "auto" else ApproxPersistChooser(7)
116
                ),
117
            )
118
        return self._bisector
6✔
119

120
    @property
6✔
121
    def build_download_manager(self):
6✔
122
        if self._build_download_manager is None:
6✔
123
            background_dl_policy = self.options.background_dl_policy
6✔
124
            if not self.options.persist:
6✔
125
                # cancel background downloads forced
126
                background_dl_policy = "cancel"
6✔
127
            self._build_download_manager = BuildDownloadManager(
6✔
128
                self._download_dir,
129
                background_dl_policy=background_dl_policy,
130
                persist_limit=PersistLimit(self.options.persist_size_limit),
131
            )
132
        return self._build_download_manager
6✔
133

134
    def bisect_nightlies(self):
6✔
135
        good_date, bad_date = self.options.good, self.options.bad
6✔
136
        handler = NightlyHandler(
6✔
137
            find_fix=self.options.find_fix,
138
            ensure_good_and_bad=self.options.mode != "no-first-check",
139
        )
140
        result = self._do_bisect(handler, good_date, bad_date)
6✔
141
        if result == Bisection.FINISHED:
6✔
142
            LOG.info("Got as far as we can go bisecting nightlies...")
6✔
143
            handler.print_range()
6✔
144
            if self.fetch_config.can_go_integration():
6✔
145
                LOG.info("Switching bisection method to taskcluster")
6✔
146
                self.fetch_config.set_repo(self.fetch_config.get_nightly_repo(handler.bad_date))
6✔
147
                return self._bisect_integration(
6✔
148
                    handler.good_revision, handler.bad_revision, expand=DEFAULT_EXPAND
149
                )
150
        elif result == Bisection.USER_EXIT:
6✔
151
            self._print_resume_info(handler)
6✔
152
        else:
153
            # NO_DATA
154
            LOG.info(
6✔
155
                "Unable to get valid builds within the given"
156
                " range. You should try to launch mozregression"
157
                " again with a larger date range."
158
            )
159
            return 1
6✔
160
        return 0
6✔
161

162
    def bisect_integration(self):
6✔
163
        return self._bisect_integration(
6✔
164
            self.options.good,
165
            self.options.bad,
166
            ensure_good_and_bad=self.options.mode != "no-first-check",
167
        )
168

169
    def _bisect_integration(self, good_rev, bad_rev, ensure_good_and_bad=False, expand=0):
6✔
170
        LOG.info(
6✔
171
            "Getting %s builds between %s and %s"
172
            % (self.fetch_config.integration_branch, good_rev, bad_rev)
173
        )
174
        if self.options.app == "firefox-snap":
6✔
NEW
175
            handler = SnapHandler(
×
176
                find_fix=self.options.find_fix, ensure_good_and_bad=ensure_good_and_bad
177
            )
178
        else:
179
            handler = IntegrationHandler(
6✔
180
                find_fix=self.options.find_fix, ensure_good_and_bad=ensure_good_and_bad
181
            )
182
        result = self._do_bisect(handler, good_rev, bad_rev, expand=expand)
6✔
183
        if result == Bisection.FINISHED:
6✔
184
            LOG.info("No more integration revisions, bisection finished.")
6✔
185
            handler.print_range()
6✔
186
            if handler.good_revision == handler.bad_revision:
6✔
187
                LOG.warning(
6✔
188
                    "It seems that you used two changesets that are in"
189
                    " the same push. Check the pushlog url."
190
                )
191
            elif len(handler.build_range) == 2:
×
192
                # range reduced to 2 pushes (at least ones with builds):
193
                # one good, one bad.
194
                result = handler.handle_merge()
×
195
                if result:
×
196
                    branch, good_rev, bad_rev = result
×
197
                    self.fetch_config.set_repo(branch)
×
198
                    return self._bisect_integration(good_rev, bad_rev, expand=DEFAULT_EXPAND)
×
199
                else:
200
                    # This code is broken, it prints out the message even when
201
                    # there are multiple bug numbers or commits in the range.
202
                    # Somebody should fix it before re-enabling it.
203
                    return 0
×
204
                    # print a bug if:
205
                    # (1) there really is only one bad push (and we're not
206
                    # just missing the builds for some intermediate builds)
207
                    # (2) there is only one bug number in that push
208
                    jp = JsonPushes(handler.build_range[1].repo_name)
209
                    num_pushes = len(
210
                        jp.pushes_within_changes(
211
                            handler.build_range[0].changeset,
212
                            handler.build_range[1].changeset,
213
                        )
214
                    )
215
                    if num_pushes == 2:
216
                        bugids = find_bugids_in_push(
217
                            handler.build_range[1].repo_name,
218
                            handler.build_range[1].changeset,
219
                        )
220
                        if len(bugids) == 1:
221
                            word = "fix" if handler.find_fix else "regression"
222
                            LOG.info(
223
                                "Looks like the following bug has the "
224
                                " changes which introduced the"
225
                                " {}:\n{}".format(word, bug_url(bugids[0]))
226
                            )
227
        elif result == Bisection.USER_EXIT:
6✔
228
            self._print_resume_info(handler)
6✔
229
        else:
230
            # NO_DATA. With integration branches, this can not happen if changesets
231
            # are incorrect - so builds are probably too old
232
            LOG.info(
6✔
233
                "There are no build artifacts for these changesets (they are probably too old)."
234
            )
235
            return 1
6✔
236
        return 0
6✔
237

238
    def _do_bisect(self, handler, good, bad, **kwargs):
6✔
239
        try:
6✔
240
            return self.bisector.bisect(handler, good, bad, **kwargs)
6✔
241
        except (KeyboardInterrupt, MozRegressionError, RequestException) as exc:
6✔
242
            if (
6✔
243
                handler.good_revision is not None
244
                and handler.bad_revision is not None
245
                and not isinstance(exc, GoodBadExpectationError)
246
            ):
247
                atexit.register(self._on_exit_print_resume_info, handler)
6✔
248
            raise
6✔
249

250
    def _print_resume_info(self, handler):
6✔
251
        # copy sys.argv, remove every --good/--bad/--repo related argument,
252
        # then add our own
253
        argv = sys.argv[:]
6✔
254
        args = ("--good", "--bad", "-g", "-b", "--good-rev", "--bad-rev", "--repo")
6✔
255
        indexes_to_remove = []
6✔
256
        for i, arg in enumerate(argv):
6✔
257
            if i in indexes_to_remove:
6✔
258
                continue
6✔
259
            for karg in args:
6✔
260
                if karg == arg:
6✔
261
                    # handle '--good 2015-01-01'
262
                    indexes_to_remove.extend((i, i + 1))
6✔
263
                    break
6✔
264
                elif arg.startswith(karg + "="):
6✔
265
                    # handle '--good=2015-01-01'
266
                    indexes_to_remove.append(i)
×
267
                    break
×
268
        for i in reversed(indexes_to_remove):
6✔
269
            del argv[i]
6✔
270

271
        argv.append("--repo=%s" % handler.build_range[0].repo_name)
6✔
272

273
        if hasattr(handler, "good_date"):
6✔
274
            argv.append("--good=%s" % handler.good_date)
6✔
275
            argv.append("--bad=%s" % handler.bad_date)
6✔
276
        else:
277
            argv.append("--good=%s" % handler.good_revision)
6✔
278
            argv.append("--bad=%s" % handler.bad_revision)
6✔
279

280
        LOG.info("To resume, run:")
6✔
281
        LOG.info(" ".join([pipes.quote(arg) for arg in argv]))
6✔
282

283
    def _on_exit_print_resume_info(self, handler):
6✔
284
        handler.print_range()
6✔
285
        self._print_resume_info(handler)
6✔
286

287
    def _launch(self, fetcher_class):
6✔
288
        fetcher = fetcher_class(self.fetch_config)
×
289
        build_info = fetcher.find_build_info(self.options.launch)
×
290
        self.build_download_manager.focus_download(build_info)
×
291
        self.test_runner.run_once(build_info)
×
292

293
    def launch_nightlies(self):
6✔
294
        self._launch(NightlyInfoFetcher)
×
295

296
    def launch_integration(self):
6✔
297
        self._launch(IntegrationInfoFetcher)
×
298

299

300
def pypi_latest_version():
6✔
301
    url = "https://pypi.python.org/pypi/mozregression/json"
6✔
302
    return requests.get(url, timeout=10).json()["info"]["version"]
6✔
303

304

305
def check_mozregression_version():
6✔
306
    try:
6✔
307
        mozregression_version = pypi_latest_version()
6✔
308
    except (RequestException, KeyError, ValueError):
6✔
309
        LOG.critical("Unable to get latest version from pypi.")
6✔
310
        return
6✔
311

312
    if __version__ != mozregression_version:
6✔
313
        LOG.warning(
6✔
314
            "You are using mozregression version %s, "
315
            "however version %s is available." % (__version__, mozregression_version)
316
        )
317

318
        LOG.warning(
6✔
319
            "You should consider upgrading via the 'pip install"
320
            " --upgrade mozregression' command."
321
        )
322

323

324
def main(
6✔
325
    argv=None,
326
    namespace=None,
327
    check_new_version=True,
328
    mozregression_variant="console",
329
):
330
    """
331
    main entry point of mozregression command line.
332
    """
333
    # terminal color support on windows
334
    if os.name == "nt":
6✔
335
        colorama.init()
×
336

337
    config, app = None, None
6✔
338
    try:
6✔
339
        config = cli(argv=argv, namespace=namespace)
6✔
340
        if check_new_version:
6✔
341
            check_mozregression_version()
6✔
342
        config.validate()
6✔
343
        set_http_session(get_defaults={"timeout": config.options.http_timeout})
6✔
344

345
        app = Application(config.fetch_config, config.options)
6✔
346
        send_telemetry_ping_oop(
6✔
347
            UsageMetrics(
348
                variant=mozregression_variant,
349
                appname=config.fetch_config.app_name,
350
                build_type=config.fetch_config.build_type,
351
                good=config.options.good,
352
                bad=config.options.bad,
353
                launch=config.options.launch,
354
                **get_system_info(),
355
            ),
356
            config.enable_telemetry,
357
        )
358

359
        method = getattr(app, config.action)
6✔
360
        sys.exit(method())
6✔
361

362
    except KeyboardInterrupt:
6✔
363
        sys.exit("\nInterrupted.")
6✔
364
    except (MozRegressionError, RequestException) as exc:
6✔
365
        if isinstance(exc, HTTPError) and exc.response.status_code == 401:
6✔
366
            # remove the taskcluster credential file - looks like it's wrong
367
            # anyway. This will force mozregression to ask again next time.
368
            mozfile.remove(TC_CREDENTIALS_FNAME)
×
369
        LOG.error(str(exc)) if config else sys.exit(str(exc))
6✔
370
        sys.exit(1)
6✔
371
    finally:
372
        if app:
6✔
373
            app.clear()
6✔
374

375

376
if __name__ == "__main__":
377
    main()
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