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)