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,
)