gui/mozregui/wizard.py (271 lines of code) (raw):

from __future__ import absolute_import import datetime import mozinfo from PySide6.QtCore import SIGNAL, QDate, QStringListModel, Qt, Slot from PySide6.QtWidgets import QApplication, QCompleter, QMessageBox, QWizard, QWizardPage from mozregression.branches import get_branches from mozregression.dates import to_datetime from mozregression.errors import DateFormatError, LauncherNotRunnable from mozregression.fetch_configs import REGISTRY, create_config from mozregression.launchers import REGISTRY as LAUNCHER_REGISTRY from .ui.build_selection import Ui_BuildSelectionPage from .ui.intro import Ui_Intro from .ui.profile import Ui_Profile from .ui.single_build_selection import Ui_SingleBuildSelectionPage def resolve_obj_name(obj, name): names = name.split(".") while names: obj = getattr(obj, names.pop(0)) return obj class WizardPage(QWizardPage): UI_CLASS = None TITLE = "" SUBTITLE = "" FIELDS = {} def __init__(self): QWizardPage.__init__(self) self.setTitle(self.TITLE) self.setSubTitle(self.SUBTITLE) self.ui = self.UI_CLASS() self.ui.setupUi(self) for name, widget_name in self.FIELDS.items(): self.registerField(name, resolve_obj_name(self.ui, widget_name)) def set_options(self, options): """ Fill the options dict argument with the page information. By default, take every field value present in the FIELDS class attribute. """ for fieldname in self.FIELDS: options[fieldname] = self.field(fieldname) class IntroPage(WizardPage): UI_CLASS = Ui_Intro TITLE = "Basic configuration" SUBTITLE = "Please choose an application and other options to specify" " what you want to test." FIELDS = { "application": "app_combo", "repository": "repository", "bits": "bits_combo", "arch": "arch_combo", "build_type": "build_type", "lang": "lang", "url": "url", } def __init__(self): WizardPage.__init__(self) self.fetch_config = None self.app_model = QStringListModel( REGISTRY.names(lambda klass: not getattr(klass, "disable_in_gui", None)) ) self.ui.app_combo.setModel(self.app_model) if mozinfo.bits == 64: if mozinfo.os == "mac": self.bits_model = QStringListModel(["64"]) bits_index = 0 else: self.bits_model = QStringListModel(["32", "64"]) bits_index = 1 elif mozinfo.bits == 32: self.bits_model = QStringListModel(["32"]) bits_index = 0 self.ui.bits_combo.setModel(self.bits_model) self.ui.bits_combo.setCurrentIndex(bits_index) self.arch_model = QStringListModel() self.build_type_model = QStringListModel() self.ui.app_combo.currentIndexChanged.connect(self._set_fetch_config) self.ui.bits_combo.currentIndexChanged.connect(self._set_fetch_config) self.ui.app_combo.setCurrentIndex(self.ui.app_combo.findText("firefox")) self.ui.repository.textChanged.connect(self._on_repo_changed) completer = QCompleter(sorted(get_branches()), self) completer.setCaseSensitivity(Qt.CaseInsensitive) self.ui.repository.setCompleter(completer) QApplication.instance().focusChanged.connect(self._on_focus_changed) def _on_repo_changed(self, text): enable_release = not text or text == "mozilla-central" build_select_page = self.wizard().page(2) if isinstance(build_select_page, SingleBuildSelectionPage): build_menus = [build_select_page.ui.build] else: build_menus = [build_select_page.ui.start, build_select_page.ui.end] for menu in build_menus: menu.ui.combo_helper.model().item(1).setEnabled(enable_release) if menu.ui.combo_helper.currentIndex() == 1: menu.ui.combo_helper.setCurrentIndex(0) def _on_focus_changed(self, old, new): # show the repository completion on focus if new == self.ui.repository and not self.ui.repository.text(): self.ui.repository.completer().complete() def _set_fetch_config(self, index): app_name = str(self.ui.app_combo.currentText()) bits = int(self.ui.bits_combo.currentText()) self.fetch_config = create_config(app_name, mozinfo.os, bits, mozinfo.processor) available_archs = self.fetch_config.available_archs() self.arch_model = QStringListModel(available_archs) self.ui.arch_combo.setModel(self.arch_model) if not self.arch_model.stringList(): self.ui.arch_label.setDisabled(True) self.ui.arch_combo.setDisabled(True) else: self.ui.arch_label.setEnabled(True) self.ui.arch_combo.setEnabled(True) if mozinfo.processor in available_archs: self.ui.arch_combo.setCurrentIndex(available_archs.index(mozinfo.processor)) self.build_type_model = QStringListModel(self.fetch_config.available_build_types()) self.ui.build_type.setModel(self.build_type_model) if not self.fetch_config.available_bits(): self.ui.bits_combo.setDisabled(True) self.ui.label_4.setDisabled(True) else: self.ui.bits_combo.setEnabled(True) self.ui.label_4.setEnabled(True) # URL doesn't make sense for Thunderbird if app_name == "thunderbird": self.ui.url.setDisabled(True) self.ui.url_label.setDisabled(True) else: self.ui.url.setEnabled(True) self.ui.url_label.setEnabled(True) # lang only makes sense for firefox-l10n and thunderbird-l10n, and repo doesn't if app_name in ("firefox-l10n", "thunderbird-l10n"): self.ui.lang.setEnabled(True) self.ui.lang_label.setEnabled(True) self.ui.repository.setDisabled(True) self.ui.repository_label.setDisabled(True) else: self.ui.lang.setDisabled(True) self.ui.lang_label.setDisabled(True) self.ui.repository.setEnabled(True) self.ui.repository_label.setEnabled(True) def validatePage(self): app_name = self.fetch_config.app_name launcher_class = LAUNCHER_REGISTRY.get(app_name) try: launcher_class.check_is_runnable() return True except LauncherNotRunnable as exc: QMessageBox.critical(self, "%s is not runnable" % app_name, str(exc)) return False class ProfilePage(WizardPage): UI_CLASS = Ui_Profile TITLE = "Profile selection" SUBTITLE = ( "Choose a specific profile. You can choose an existing profile" ", or let this blank to use a new one." ) FIELDS = { "profile": "profile_widget.line_edit", "profile_persistence": "profile_persistence_combo", } def __init__(self): WizardPage.__init__(self) profile_persistence_options = ["clone", "clone-first", "reuse"] self.profile_persistence_model = QStringListModel(profile_persistence_options) self.ui.profile_persistence_combo.setModel(self.profile_persistence_model) self.ui.profile_persistence_combo.setCurrentIndex(0) def set_options(self, options): WizardPage.set_options(self, options) # get the prefs options["preferences"] = self.get_prefs() # get the addons options["addons"] = self.get_addons() # get the profile-persistence options["profile_persistence"] = self.get_profile_persistence() def get_prefs(self): return self.ui.pref_widget.get_prefs() def get_addons(self): return self.ui.addons_widget.get_addons() def get_profile_persistence(self): return self.ui.profile_persistence_combo.currentText() class BuildSelectionPage(WizardPage): UI_CLASS = Ui_BuildSelectionPage TITLE = "Build selection" SUBTITLE = "Select the range to bisect." FIELDS = {"find_fix": "find_fix"} def __init__(self): WizardPage.__init__(self) now = QDate.currentDate() self.ui.start.ui.date.setDate(now.addYears(-1)) self.ui.end.ui.date.setDate(now) self.ui.find_fix.stateChanged.connect(self.change_labels) def set_options(self, options): WizardPage.set_options(self, options) options["good"] = self.get_start() options["bad"] = self.get_end() if options["find_fix"]: options["good"], options["bad"] = options["bad"], options["good"] @Slot() def change_labels(self): find_fix = self.ui.find_fix.isChecked() if find_fix: self.ui.label.setText("Last known bad build") self.ui.label_2.setText("First known good build") else: self.ui.label.setText("Last known good build") self.ui.label_2.setText("First known bad build") def get_start(self): return self.ui.start.get_value() def get_end(self): return self.ui.end.get_value() def validatePage(self): start, end = self.get_start(), self.get_end() if isinstance(start, str) or isinstance(end, str): # do not check revisions return True try: start_date = to_datetime(start) end_date = to_datetime(end) except DateFormatError as exc: QMessageBox.critical(self, "Error", str(exc)) return False current = datetime.datetime.now() if start_date < end_date: if end_date <= current: return True else: QMessageBox.critical(self, "Error", "You can't define a date in the future.") else: QMessageBox.critical( self, "Error", "The first date must be earlier than the second one." ) return False class Wizard(QWizard): def __init__(self, title, class_pages, parent=None): QWizard.__init__(self, parent) self.setWindowTitle(title) self.resize(800, 600) # associate current text to comboboxes fields instead of current index self.setDefaultProperty("QComboBox", "currentText", SIGNAL("currentIndexChanged(QString)")) for klass in class_pages: self.addPage(klass()) def options(self): options = {} for page_id in self.pageIds(): self.page(page_id).set_options(options) fetch_config = self.page(self.pageIds()[0]).fetch_config fetch_config.set_repo(options["repository"]) if options["arch"]: fetch_config.set_arch(options["arch"]) fetch_config.set_build_type(options["build_type"]) # create a profile if required launcher_class = LAUNCHER_REGISTRY.get(fetch_config.app_name) if options["profile_persistence"] in ("clone-first", "reuse"): options["profile"] = launcher_class.create_profile( profile=options["profile"], addons=options["addons"], preferences=options["preferences"], clone=options["profile_persistence"] == "clone-first", ) return fetch_config, options class BisectionWizard(Wizard): def __init__(self, parent=None): Wizard.__init__( self, "Bisection wizard", (IntroPage, ProfilePage, BuildSelectionPage), parent=parent, ) class SingleBuildSelectionPage(WizardPage): UI_CLASS = Ui_SingleBuildSelectionPage TITLE = "Build selection" SUBTITLE = "Select the build you want to run." def __init__(self): WizardPage.__init__(self) now = QDate.currentDate() self.ui.build.ui.date.setDate(now.addDays(-3)) def set_options(self, options): WizardPage.set_options(self, options) options["launch"] = self.ui.build.get_value() class SingleRunWizard(Wizard): def __init__(self, parent=None): Wizard.__init__( self, "Single run wizard", (IntroPage, ProfilePage, SingleBuildSelectionPage), parent=parent, )