gui/mozregui/build_runner.py (160 lines of code) (raw):

from PySide6.QtCore import QObject, QThread, QTimer, Signal, Slot from mozregression.download_manager import BuildDownloadManager from mozregression.errors import LauncherError, MozRegressionError from mozregression.network import get_http_session from mozregression.persist_limit import PersistLimit from mozregression.telemetry import UsageMetrics, get_system_info, send_telemetry_ping from mozregression.test_runner import create_launcher from mozregui.global_prefs import apply_prefs, get_prefs from mozregui.log_report import log class GuiBuildDownloadManager(QObject, BuildDownloadManager): download_progress = Signal(object, int, int) download_started = Signal(object) download_finished = Signal(object, str) def __init__(self, destdir, persist_limit, **kwargs): super().__init__( destdir=destdir, session=get_http_session(), persist_limit=persist_limit, **kwargs ) def _download_started(self, task): self.download_started.emit(task) BuildDownloadManager._download_started(self, task) def _download_finished(self, task): try: self.download_finished.emit(task, task.get_dest()) except RuntimeError: # in some cases, closing the application may destroy the # underlying c++ QObject, causing this signal to fail. # Skip this silently. pass BuildDownloadManager._download_finished(self, task) def focus_download(self, build_info): build_url, fname = self._extract_download_info(build_info) dest = self.get_dest(fname) build_info.build_file = dest # first, stop all downloads in background (except the one for this # build if any) self.cancel(cancel_if=lambda dl: dest != dl.get_dest()) dl = self.download(build_url, fname, progress=self.download_progress.emit) if not dl: # file already downloaded. # emit the finished signal so bisection goes on self.download_finished.emit(None, dest) class GuiTestRunner(QObject): evaluate_started = Signal(str) evaluate_finished = Signal() def __init__(self): super().__init__() self.verdict = None self.launcher = None self.launcher_kwargs = {} self.run_error = False def evaluate(self, build_info, allow_back=False): try: self.launcher = create_launcher(build_info) self.launcher.start(**self.launcher_kwargs) build_info.update_from_app_info(self.launcher.get_app_info()) except Exception as exc: self.run_error = True self.evaluate_started.emit(str(exc)) else: self.evaluate_started.emit("") self.run_error = False def finish(self, verdict): if self.launcher: try: self.launcher.stop() except LauncherError: pass # silently pass stop process error self.launcher.cleanup() self.verdict = verdict if verdict is not None: self.evaluate_finished.emit() class AbstractBuildRunner(QObject): """ Base class to run a build. Create the required test runner and build manager, along with a thread that should be used for blocking tasks. """ running_state_changed = Signal(bool) worker_created = Signal(object) worker_class = None def __init__(self, mainwindow): super().__init__() self.mainwindow = mainwindow self.thread = None self.worker = None self.pending_threads = [] self.test_runner = None self.download_manager = None self.options = None self.stopped = False def init_worker(self, fetch_config, options): """ Create and initialize the worker. Should be subclassed to configure the worker, and should return the worker method that should start the work. """ self.options = options # global preferences global_prefs = get_prefs() self.global_prefs = global_prefs # apply the global prefs now apply_prefs(global_prefs) fetch_config.set_base_url(global_prefs["archive_base_url"]) download_dir = global_prefs["persist"] if not download_dir: download_dir = self.mainwindow.persist persist_limit = PersistLimit(abs(global_prefs["persist_size_limit"]) * 1073741824) self.download_manager = GuiBuildDownloadManager(download_dir, persist_limit) self.test_runner = GuiTestRunner() self.thread = QThread() # options for the app launcher launcher_kwargs = {} for name in ("profile", "preferences"): if name in options: value = options[name] if value: launcher_kwargs[name] = value # add add-ons paths to the app launcher launcher_kwargs["addons"] = options["addons"] self.test_runner.launcher_kwargs = launcher_kwargs launcher_kwargs["cmdargs"] = [] if options["profile_persistence"] in ("clone-first", "reuse") or options["profile"]: launcher_kwargs["cmdargs"] += ["--allow-downgrade"] # Thunderbird will fail to start if passed an URL arg if options.get("url") and fetch_config.app_name != "thunderbird": launcher_kwargs["cmdargs"] += [options["url"]] # Lang only works for firefox-l10n and thunderbird-l10n. if options.get("lang"): if options["application"] in ("firefox-l10n", "thunderbird-l10n"): fetch_config.set_lang(options["lang"]) else: raise MozRegressionError("Invalid lang argument") self.worker = self.worker_class(fetch_config, self.test_runner, self.download_manager) # Move self.bisector in the thread. This will # allow to the self.bisector slots (connected after the move) # to be automatically called in the thread. self.worker.moveToThread(self.thread) self.worker_created.emit(self.worker) def start(self, fetch_config, options): action = self.init_worker(fetch_config, options) assert callable(action), "%s should be callable" % action self.thread.start() # this will be called in the worker thread. QTimer.singleShot(0, action) # an action = instance of mozregression usage, so send # a usage ping (if telemetry is disabled, it will automatically # be discarded) send_telemetry_ping( UsageMetrics( variant="gui", appname=fetch_config.app_name, build_type=fetch_config.build_type, good=options.get("good"), bad=options.get("bad"), launch=getattr(self.worker, "launch_arg", None), **get_system_info(), ) ) self.stopped = False self.running_state_changed.emit(True) @Slot() def stop(self, wait=True): self.stopped = True if self.options: if self.options["profile"] and self.options["profile_persistence"] == "clone-first": self.options["profile"].cleanup() if self.download_manager: self.download_manager.cancel() if self.thread: self.thread.quit() if wait: if self.download_manager: self.download_manager.wait(raise_if_error=False) if self.thread: # wait for thread(s) completion - this is the case when # user close the application self.thread.wait() for thread in self.pending_threads: thread.wait() self.thread = None elif self.thread: # do not block, just keep track of the thread - we got here # when user uses the stop button. self.pending_threads.append(self.thread) self.thread.finished.connect(self._remove_pending_thread) if self.test_runner: self.test_runner.finish(None) self.running_state_changed.emit(False) log("Stopped") @Slot() def _remove_pending_thread(self): for thread in self.pending_threads[:]: if thread.isFinished(): self.pending_threads.remove(thread)