gui/mozregui/report.py (245 lines of code) (raw):

from PySide6.QtCore import QAbstractTableModel, QModelIndex, Qt, QUrl, Signal, Slot from PySide6.QtGui import QColor, QDesktopServices from PySide6.QtWidgets import QTableView, QTextBrowser from mozregression.bisector import NightlyHandler from mozregui.utils import is_dark_mode_enabled # Custom colors GRAY_WHITE = QColor(243, 243, 243) DARK_GRAY = QColor(28, 28, 28) VERDICT_TO_ROW_COLORS = { "g": QColor(152, 251, 152), # light green "b": QColor(250, 113, 113), # light red "s": QColor(253, 248, 107), # light yellow "r": QColor(225, 225, 225), # light gray "g_dark": QColor(48, 209, 88), # green "b_dark": QColor(255, 70, 58), # red "s_dark": QColor(160, 90, 0), # yellow "r_dark": QColor(45, 45, 45), # gray } class ReportItem(object): """ A base item in the report view """ def __init__(self): self.data = {} self.downloading = False self.waiting_evaluation = False self.progress = 0 def update_pushlogurl(self, bisection): if bisection.handler.found_repo: self.data["pushlog_url"] = bisection.handler.get_pushlog_url() else: self.data["pushlog_url"] = "Not available" self.data["repo_name"] = bisection.build_range[0].repo_name def status_text(self): return "Looking for build data..." def set_progress(self, current, total): self.progress = (current * 100) / total class StartItem(ReportItem): """ Report a started bisection """ def update_pushlogurl(self, bisection): ReportItem.update_pushlogurl(self, bisection) handler = bisection.handler if isinstance(handler, NightlyHandler): self.build_type = "nightly" else: self.build_type = "integration" if self.build_type == "nightly": self.first, self.last = handler.get_date_range() else: self.first, self.last = handler.get_range() self.first = self.first[:8] self.last = self.last[:8] if handler.find_fix: self.first, self.last = self.last, self.first def status_text(self): if "pushlog_url" not in self.data: return ReportItem.status_text(self) return "Bisecting on %s [%s - %s]" % ( self.data["repo_name"], self.first, self.last, ) class StepItem(ReportItem): """ Report a bisection step """ def __init__(self): ReportItem.__init__(self) self.state_text = "Found" self.verdict = None def status_text(self): if not self.data: return ReportItem.status_text(self) if self.data["build_type"] == "nightly": desc = self.data["build_date"] else: desc = self.data["changeset"][:8] if self.verdict is not None: desc = "%s (verdict: %s)" % (desc, self.verdict) return "%s %s build: %s" % (self.state_text, self.data["repo_name"], desc) def _bulk_action_slots(action, slots, signal_object, slot_object): for name in slots: signal = getattr(signal_object, name) slot = getattr(slot_object, name) getattr(signal, action)(slot) class ReportModel(QAbstractTableModel): need_evaluate_editor = Signal(bool, QModelIndex) def __init__(self): QAbstractTableModel.__init__(self) self.items = [] self.bisector = None self.single_runner = None def clear(self): self.beginResetModel() self.items = [] self.endResetModel() @Slot(object) def attach_bisector(self, bisector): bisector_slots = ( "step_started", "step_build_found", "step_testing", "step_finished", "started", "finished", ) downloader_slots = ("download_progress",) if bisector: self.attach_single_runner(None) _bulk_action_slots("connect", bisector_slots, bisector, self) _bulk_action_slots("connect", downloader_slots, bisector.download_manager, self) self.bisector = bisector @Slot(object) def attach_single_runner(self, single_runner): sr_slots = ("started", "step_build_found", "step_testing") downloader_slots = ("download_progress",) if single_runner: self.attach_bisector(None) _bulk_action_slots("connect", sr_slots, single_runner, self) _bulk_action_slots("connect", downloader_slots, single_runner.download_manager, self) self.single_runner = single_runner @Slot(object, int, int) def download_progress(self, dl, current, total): item = self.items[-1] item.state_text = "Downloading" item.downloading = True item.set_progress(current, total) self.update_item(item) def get_item(self, index): return self.items[index.row()] def rowCount(self, parent=QModelIndex()): return len(self.items) def columnCount(self, parent=QModelIndex()): return 1 def data(self, index, role=Qt.DisplayRole): item = self.items[index.row()] if role == Qt.DisplayRole: return item.status_text() elif role == Qt.BackgroundRole: if isinstance(item, StepItem) and item.verdict: if is_dark_mode_enabled(): return VERDICT_TO_ROW_COLORS.get(str(item.verdict) + "_dark", DARK_GRAY) else: return VERDICT_TO_ROW_COLORS.get(str(item.verdict), GRAY_WHITE) elif is_dark_mode_enabled(): return DARK_GRAY else: return GRAY_WHITE return None def update_item(self, item): index = self.createIndex(self.items.index(item), 0) self.dataChanged.emit(index, index) def append_item(self, item): row = self.rowCount() self.beginInsertRows(QModelIndex(), row, row) self.items.append(item) self.endInsertRows() @Slot() def started(self): # when a bisection starts, insert an item to report it self.append_item(StartItem()) @Slot(object, int) def step_started(self, bisection): last_item = self.items[-1] if isinstance(last_item, StepItem): # update the pushlog for the start step if hasattr(bisection, "handler"): last_item.update_pushlogurl(bisection) self.update_item(last_item) # and add a new step self.append_item(StepItem()) @Slot(object, int, object) def step_build_found(self, bisection, build_infos): last_item = self.items[-1] if isinstance(last_item, StartItem): # update the pushlog for the start step if hasattr(bisection, "handler"): last_item.update_pushlogurl(bisection) self.update_item(last_item) else: # single runner case # TODO: rework report.py implementation... self.finished(None, None) # remove last item # and add the new step with build_infos item = StepItem() item.data.update(build_infos.to_dict()) self.append_item(item) else: # previous item is a step, just update it last_item.data.update(build_infos.to_dict()) self.update_item(last_item) @Slot(object, int, object) def step_testing(self, bisection, build_infos): last_item = self.items[-1] last_item.downloading = False last_item.waiting_evaluation = True last_item.state_text = "Testing" # we may have more build data now that the build has been installed last_item.data.update(build_infos.to_dict()) if hasattr(bisection, "handler"): last_item.update_pushlogurl(bisection) self.update_item(last_item) if hasattr(bisection, "handler"): # not a single runner index = self.createIndex(self.rowCount() - 1, 0) self.need_evaluate_editor.emit(True, index) @Slot(object, int, str) def step_finished(self, bisection, verdict): # step finished, just store the verdict item = self.items[-1] item.waiting_evaluation = False item.state_text = "Tested" item.verdict = verdict self.update_item(item) if hasattr(bisection, "handler"): # not a single runner index = self.createIndex(self.rowCount() - 1, 0) self.need_evaluate_editor.emit(False, index) @Slot(object, int) def finished(self, bisection, result): # remove the last inserted step if not self.items[-1].data: index = len(self.items) - 1 self.beginRemoveRows(QModelIndex(), index, index) self.items.pop(index) self.endRemoveRows() class ReportView(QTableView): step_report_changed = Signal(object) def __init__(self, parent=None): QTableView.__init__(self, parent) self._model = ReportModel() self.setModel(self._model) self._model.dataChanged.connect(self.on_item_changed) def currentChanged(self, current, previous): if current.row() >= 0: item = self._model.items[current.row()] self.step_report_changed.emit(item) @Slot(QModelIndex, QModelIndex) def on_item_changed(self, top_left, bottom_right): if self.currentIndex().row() == top_left.row(): item = self._model.items[top_left.row()] # while an item is downloaded, the underlying data model # change a lot only to update the download progress state. # It becomes impossible then to scroll the # BuildInfoTextBrowser, so we do not want to update # that when we are downloading. if not item.downloading: self.step_report_changed.emit(item) class BuildInfoTextBrowser(QTextBrowser): def __init__(self, parent=None): QTextBrowser.__init__(self, parent) self.anchorClicked.connect(self.on_anchor_clicked) @Slot(object) def update_content(self, item): if not item.data: self.clear() return html = "" for k in sorted(item.data): v = item.data[k] if v is not None: html += "<strong>%s</strong>: " % k if isinstance(v, str): url = QUrl(v) if url.isValid() and url.scheme(): v = '<a href="%s">%s</a>' % (v, v) html += "{}<br>".format(v) self.setHtml(html) @Slot(QUrl) def on_anchor_clicked(self, url): QDesktopServices.openUrl(url)