[GUI] create an AbstractBuildRunner class

This will allow to reuse the code for the single launch build.
This commit is contained in:
Julien Pagès 2015-12-06 10:37:35 +01:00
Родитель 6829cdb708
Коммит 41141a8b96
4 изменённых файлов: 220 добавлений и 182 удалений

Просмотреть файл

@ -1,100 +1,20 @@
import sys
from PyQt4.QtCore import QObject, pyqtSignal as Signal, pyqtSlot as Slot, \
QThread, QTimer
QTimer
from PyQt4.QtGui import QMessageBox, QDialog, QRadioButton
from mozregression.bisector import Bisector, Bisection, NightlyHandler, \
InboundHandler
from mozregression.download_manager import BuildDownloadManager
from mozregression.test_runner import TestRunner
from mozregression.errors import MozRegressionError, LauncherError
from mozregression.network import get_http_session
from mozregression.persist_limit import PersistLimit
from mozregression.errors import MozRegressionError
from mozregression.dates import is_date_or_datetime
from mozregui.build_runner import AbstractBuildRunner
from mozregui.ui.verdict import Ui_Verdict
from mozregui.global_prefs import get_prefs, apply_prefs
from mozregui.skip_chooser import SkipDialog
Bisection.EXCEPTION = -1 # new possible value of bisection end
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):
QObject.__init__(self)
persist_limit = PersistLimit(persist_limit)
BuildDownloadManager.__init__(self, 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)
if dl:
dl.set_progress(self.download_progress.emit)
else:
# file already downloaded.
# emit the finished signal so bisection goes on
self.download_finished.emit(None, dest)
class GuiTestRunner(QObject, TestRunner):
evaluate_started = Signal(str)
evaluate_finished = Signal()
def __init__(self):
QObject.__init__(self)
TestRunner.__init__(self)
self.verdict = None
self.launcher = None
self.launcher_kwargs = {}
def evaluate(self, build_info, allow_back=False):
try:
self.launcher = self.create_launcher(build_info)
self.launcher.start(**self.launcher_kwargs)
build_info.update_from_app_info(self.launcher.get_app_info())
except LauncherError, exc:
self.evaluate_started.emit(str(exc))
else:
self.evaluate_started.emit('')
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
self.evaluate_finished.emit()
class GuiBisector(QObject, Bisector):
started = Signal()
finished = Signal(object, int)
@ -105,10 +25,9 @@ class GuiBisector(QObject, Bisector):
step_finished = Signal(object, str)
handle_merge = Signal(object, str, str, str)
def __init__(self, fetch_config, download_dir, persist_limit):
def __init__(self, fetch_config, test_runner, download_manager):
QObject.__init__(self)
Bisector.__init__(self, fetch_config, GuiTestRunner(),
GuiBuildDownloadManager(download_dir, persist_limit))
Bisector.__init__(self, fetch_config, test_runner, download_manager)
self.bisection = None
self.mid = None
self.build_infos = None
@ -246,44 +165,16 @@ def get_verdict(parent=None):
return radiobox.objectName()
class BisectRunner(QObject):
bisector_created = Signal(object)
running_state_changed = Signal(bool)
class BisectRunner(AbstractBuildRunner):
worker_class = GuiBisector
def __init__(self, mainwindow):
QObject.__init__(self)
self.mainwindow = mainwindow
self.bisector = None
self.thread = None
self.pending_threads = []
def init_worker(self, fetch_config, options):
AbstractBuildRunner.init_worker(self, fetch_config, options)
def bisect(self, fetch_config, options):
self.stop()
# global preferences
global_prefs = get_prefs()
# apply the global prefs now
apply_prefs(global_prefs)
download_dir = global_prefs['persist']
persist_limit = int(abs(global_prefs['persist_size_limit'])
* 1073741824)
if not download_dir:
download_dir = self.mainwindow.persist
self.bisector = GuiBisector(fetch_config,
download_dir, persist_limit)
# create a QThread, and move self.bisector in it. This will
# allow to the self.bisector slots (connected after the move)
# to be automatically called in the thread.
self.thread = QThread()
self.bisector.moveToThread(self.thread)
self.bisector.test_runner.evaluate_started.connect(
self.evaluate)
self.bisector.finished.connect(self.bisection_finished)
self.bisector.handle_merge.connect(self.handle_merge)
self.bisector.choose_next_build.connect(self.choose_next_build)
self.bisector_created.emit(self.bisector)
self.worker.test_runner.evaluate_started.connect(self.evaluate)
self.worker.finished.connect(self.bisection_finished)
self.worker.handle_merge.connect(self.handle_merge)
self.worker.choose_next_build.connect(self.choose_next_build)
good, bad = options.pop('good'), options.pop('bad')
if is_date_or_datetime(good) and is_date_or_datetime(bad) \
@ -292,52 +183,14 @@ class BisectRunner(QObject):
else:
handler = InboundHandler(find_fix=options['find_fix'])
# 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.bisector.test_runner.launcher_kwargs = launcher_kwargs
self.thread.start()
self.bisector._bisect_args = (handler, good, bad)
# this will be called in the worker thread.
QTimer.singleShot(0, self.bisector.bisect)
self.running_state_changed.emit(True)
self.worker._bisect_args = (handler, good, bad)
return self.worker.bisect
@Slot()
def stop(self, wait=True):
if self.bisector:
self.bisector.finished.disconnect(self.bisection_finished)
self.bisector.download_manager.cancel()
self.bisector = None
if self.thread:
self.thread.quit()
if wait:
# 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()
else:
# do not block, just keep track of the thread - we got here
# when user cancel the bisection with the button.
self.pending_threads.append(self.thread)
self.thread.finished.connect(self._remove_pending_thread)
self.thread = None
self.running_state_changed.emit(False)
@Slot()
def _remove_pending_thread(self):
for thread in self.pending_threads[:]:
if thread.isFinished():
self.pending_threads.remove(thread)
if self.worker:
self.worker.finished.disconnect(self.bisection_finished)
AbstractBuildRunner.stop(self, wait=wait)
@Slot(str)
def evaluate(self, err_message):
@ -352,13 +205,13 @@ class BisectRunner(QObject):
% err_message)
)
verdict = 's'
self.bisector.test_runner.finish(verdict)
self.worker.test_runner.finish(verdict)
@Slot()
def choose_next_build(self):
dlg = SkipDialog(self.bisector.bisection.build_range)
self.bisector._next_build_index = dlg.choose_next_build()
QTimer.singleShot(0, self.bisector._bisect_next)
dlg = SkipDialog(self.worker.bisection.build_range)
self.worker._next_build_index = dlg.choose_next_build()
QTimer.singleShot(0, self.worker._bisect_next)
@Slot(object, int)
def bisection_finished(self, bisection, resultcode):
@ -369,20 +222,20 @@ class BisectRunner(QObject):
msg = "Unable to find enough data to bisect."
dialog = QMessageBox.warning
elif resultcode == Bisection.EXCEPTION:
msg = "Error: %s" % self.bisector.error[1]
msg = "Error: %s" % self.worker.error[1]
dialog = QMessageBox.critical
else:
fetch_config = self.bisector.fetch_config
fetch_config = self.worker.fetch_config
if fetch_config.can_go_inbound() and not \
getattr(bisection, 'no_more_merge', False):
if isinstance(bisection.handler, NightlyHandler):
handler = bisection.handler
fetch_config.set_repo(
fetch_config.get_nightly_repo(handler.bad_date))
QTimer.singleShot(0, self.bisector.bisect_further)
QTimer.singleShot(0, self.worker.bisect_further)
else:
# check merge, try to bisect further
QTimer.singleShot(0, self.bisector.check_merge)
QTimer.singleShot(0, self.worker.check_merge)
return
else:
# no inbound, bisection is done.
@ -401,9 +254,9 @@ class BisectRunner(QObject):
% branch,
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes):
self.bisector.fetch_config.set_repo(str(branch))
self.worker.fetch_config.set_repo(str(branch))
bisection.handler.good_revision = str(good_rev)
bisection.handler.bad_revision = str(bad_rev)
QTimer.singleShot(0, self.bisector.bisect_further)
QTimer.singleShot(0, self.worker.bisect_further)
else:
self.stop()

Просмотреть файл

@ -0,0 +1,184 @@
from PyQt4.QtCore import QObject, QThread, pyqtSignal as Signal, \
pyqtSlot as Slot, QTimer
from mozregui.global_prefs import get_prefs, apply_prefs
from mozregression.download_manager import BuildDownloadManager
from mozregression.test_runner import TestRunner
from mozregression.network import get_http_session
from mozregression.persist_limit import PersistLimit
from mozregression.errors import LauncherError
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):
QObject.__init__(self)
persist_limit = PersistLimit(persist_limit)
BuildDownloadManager.__init__(self, 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)
if dl:
dl.set_progress(self.download_progress.emit)
else:
# file already downloaded.
# emit the finished signal so bisection goes on
self.download_finished.emit(None, dest)
class GuiTestRunner(QObject, TestRunner):
evaluate_started = Signal(str)
evaluate_finished = Signal()
def __init__(self):
QObject.__init__(self)
TestRunner.__init__(self)
self.verdict = None
self.launcher = None
self.launcher_kwargs = {}
def evaluate(self, build_info, allow_back=False):
try:
self.launcher = self.create_launcher(build_info)
self.launcher.start(**self.launcher_kwargs)
build_info.update_from_app_info(self.launcher.get_app_info())
except LauncherError, exc:
self.evaluate_started.emit(str(exc))
else:
self.evaluate_started.emit('')
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
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):
QObject.__init__(self)
self.mainwindow = mainwindow
self.thread = None
self.worker = None
self.pending_threads = []
self.test_runner = GuiTestRunner()
self.download_manager = None
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.stop()
# global preferences
global_prefs = get_prefs()
# apply the global prefs now
apply_prefs(global_prefs)
download_dir = global_prefs['persist']
if not download_dir:
download_dir = self.mainwindow.persist
persist_limit = int(abs(global_prefs['persist_size_limit'])
* 1073741824)
self.download_manager = GuiBuildDownloadManager(download_dir,
persist_limit)
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
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)
self.running_state_changed.emit(True)
@Slot()
def stop(self, wait=True):
self.test_runner.finish(None)
if self.download_manager:
self.download_manager.cancel()
if self.thread:
self.thread.quit()
if wait:
# 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()
else:
# 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)
self.thread = None
self.running_state_changed.emit(False)
@Slot()
def _remove_pending_thread(self):
for thread in self.pending_threads[:]:
if thread.isFinished():
self.pending_threads.remove(thread)

Просмотреть файл

@ -55,7 +55,7 @@ class MainWindow(QMainWindow):
self.bisect_runner = BisectRunner(self)
self.bisect_runner.bisector_created.connect(
self.bisect_runner.worker_created.connect(
self.ui.report_view.model().attach_bisector)
self.ui.report_view.step_report_changed.connect(
@ -94,7 +94,7 @@ class MainWindow(QMainWindow):
wizard = BisectionWizard(self)
if wizard.exec_() == wizard.Accepted:
self.ui.report_view.model().clear()
self.bisect_runner.bisect(*wizard.options())
self.bisect_runner.start(*wizard.options())
@Slot()
def stop_bisection(self):

Просмотреть файл

@ -6,7 +6,7 @@ import os
from mock import Mock, patch
from . import wait_signal
from mozregui import bisection
from mozregui import build_runner
from mozregression.persist_limit import PersistLimit
@ -36,7 +36,7 @@ class TestGuiBuildDownloadManager(unittest.TestCase):
tpersist_size = PersistLimit(10 * 1073741824)
self.addCleanup(shutil.rmtree, tmpdir)
self.dl_manager = \
bisection.GuiBuildDownloadManager(tmpdir, tpersist_size)
build_runner.GuiBuildDownloadManager(tmpdir, tpersist_size)
self.dl_manager.session = self.session
self.signals = {}
for sig in ('download_progress', 'download_started',
@ -44,7 +44,8 @@ class TestGuiBuildDownloadManager(unittest.TestCase):
self.signals[sig] = Mock()
getattr(self.dl_manager, sig).connect(self.signals[sig])
@patch('mozregui.bisection.GuiBuildDownloadManager._extract_download_info')
@patch(
'mozregui.build_runner.GuiBuildDownloadManager._extract_download_info')
def test_focus_download(self, extract_info):
extract_info.return_value = ('http://foo', 'foo')
mock_response(self.session_response, 'this is some data' * 10000, 0.01)
@ -70,11 +71,11 @@ class TestGuiTestRunner(unittest.TestCase):
def setUp(self):
self.evaluate_started = Mock()
self.evaluate_finished = Mock()
self.test_runner = bisection.GuiTestRunner()
self.test_runner = build_runner.GuiTestRunner()
self.test_runner.evaluate_started.connect(self.evaluate_started)
self.test_runner.evaluate_finished.connect(self.evaluate_finished)
@patch('mozregui.bisection.GuiTestRunner.create_launcher')
@patch('mozregui.build_runner.GuiTestRunner.create_launcher')
def test_basic(self, create_launcher):
launcher = Mock(get_app_info=lambda: 'app_info')
create_launcher.return_value = launcher