Skip to content

Commit

Permalink
Add an MSS updater (Open-MSS#984)
Browse files Browse the repository at this point in the history
* Updater Step 1

* Better workflow

* Finish updater

* Add tests

* Add backups, use environment only as last resort

* Mock Worker, test environment code

* Make process more transparent and safe

* Backup only at replacement, warn user on replacement

* Warn the user even more in case of environment replacement

* More tests

* Remove environment replacement

* Make restarter work on windows

* Move updater to utils

* Add command line update arguments

Co-authored-by: ReimarBauer <rb.proj@gmail.com>
  • Loading branch information
Marilyth and ReimarBauer committed Jun 17, 2021
1 parent 5262631 commit 890ef74
Show file tree
Hide file tree
Showing 13 changed files with 642 additions and 10 deletions.
17 changes: 16 additions & 1 deletion mslib/mscolab/mscolab.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
from mslib.mscolab.seed import seed_data, add_user, add_all_users_default_project,\
add_all_users_to_all_projects, delete_user
from mslib.mscolab.utils import create_files
from mslib.utils import setup_logging
from mslib.utils import setup_logging, Worker, Updater


def handle_start(args):
Expand Down Expand Up @@ -92,6 +92,7 @@ def handle_db_seed():
def main():
parser = argparse.ArgumentParser()
parser.add_argument("-v", "--version", help="show version", action="store_true", default=False)
parser.add_argument("--update", help="Updates MSS to the newest version", action="store_true", default=False)

subparsers = parser.add_subparsers(help='Available actions', dest='action')

Expand Down Expand Up @@ -125,6 +126,20 @@ def main():
print("Version:", __version__)
sys.exit()

updater = Updater()
if args.update:
updater.on_update_available.connect(lambda old, new: updater.update_mss())
updater.on_log_update.connect(lambda s: print(s.replace("\n", "")))
updater.on_status_update.connect(lambda s: print(s.replace("\n", "")))
updater.run()
while Worker.workers:
list(Worker.workers)[0].wait()
sys.exit()

updater.on_update_available.connect(lambda old, new: logging.info(f"MSS can be updated from {old} to {new}.\nRun"
" the --update argument to update the server."))
updater.run()

if args.action == "start":
handle_start(args)

Expand Down
3 changes: 3 additions & 0 deletions mslib/msui/_tests/test_mss_pyui.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ def teardown(self):
self.application.quit()
QtWidgets.QApplication.processEvents()

def test_no_updater(self):
assert not hasattr(self.window, "updater")

@mock.patch("PyQt5.QtWidgets.QMessageBox")
def test_app_start(self, mockbox):
assert mockbox.critical.call_count == 0
Expand Down
155 changes: 155 additions & 0 deletions mslib/msui/_tests/test_updater.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# -*- coding: utf-8 -*-
"""
mslib.msui._tests.test_updater
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This module provides pytest functions to tests msui.updater
This file is part of mss.
:copyright: Copyright 2021 May Bär
:copyright: Copyright 2021 by the mss team, see AUTHORS.
:license: APACHE-2.0, see LICENSE for details.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import sys
import mock
from PyQt5 import QtWidgets, QtTest

from mslib.msui.updater import UpdaterUI, Updater
from mslib.utils import Worker


def no_conda(args=None, **named_args):
raise FileNotFoundError


class SubprocessDifferentVersionMock:
def __init__(self, args=None, **named_args):
self.returncode = 0
self.args = args
if args and "list" in args and "mss" in args:
self.stdout = "*mss 0.0.0\n"
else:
self.stdout = "*mss 999.999.999\n"


class SubprocessSameMock:
def __init__(self, args=None, **named_args):
self.stdout = "*mss 999.999.999\n"
self.returncode = 0
self.args = args


def create_mock(function, on_success=None, on_failure=None, start=True):
worker = Worker(function)
if on_success:
worker.finished.connect(on_success)
if on_failure:
worker.failed.connect(on_failure)
if start:
worker.run()
return worker


class Test_MSS_ShortcutDialog:
def setup(self):
self.updater = Updater()
self.status = ""
self.update_available = False
self.update_finished = False

def update_signal(old, new):
self.update_available = True

def update_finished_signal():
self.update_finished = True

def status_signal(s):
self.status = s

self.updater.on_update_available.connect(update_signal)
self.updater.on_status_update.connect(status_signal)
self.updater.on_update_finished.connect(update_finished_signal)
self.application = QtWidgets.QApplication(sys.argv)

def teardown(self):
self.application.quit()
QtWidgets.QApplication.processEvents()

@mock.patch("subprocess.Popen", new=SubprocessDifferentVersionMock)
@mock.patch("subprocess.run", new=SubprocessDifferentVersionMock)
@mock.patch("mslib.utils.Worker.create", create_mock)
def test_update_recognised(self):
self.updater.run()

assert self.updater.new_version == "999.999.999"
assert self.update_available
self.updater.new_version = "0.0.0"

self.updater.update_mss()
assert self.status == "Update successful. Please restart MSS."
assert self.update_finished

@mock.patch("subprocess.Popen", new=SubprocessSameMock)
@mock.patch("subprocess.run", new=SubprocessSameMock)
@mock.patch("mslib.utils.Worker.create", create_mock)
def test_no_update(self):
self.updater.run()
assert self.status == "Your MSS is up to date."
assert not self.update_available
assert not self.update_finished

@mock.patch("subprocess.Popen", new=SubprocessDifferentVersionMock)
@mock.patch("subprocess.run", new=SubprocessDifferentVersionMock)
@mock.patch("mslib.utils.Worker.create", create_mock)
def test_update_failed(self):
self.updater.run()
assert self.updater.new_version == "999.999.999"
assert self.update_available
self.updater.new_version = "1000.1000.1000"
self.updater.update_mss()
assert self.status == "Update failed. Please try it manually or " \
"by creating a new environment!"

@mock.patch("subprocess.Popen", new=no_conda)
@mock.patch("subprocess.run", new=no_conda)
@mock.patch("mslib.utils.Worker.create", create_mock)
def test_no_conda(self):
self.updater.run()
assert self.updater.new_version is None and self.updater.old_version is None
assert not self.update_available
assert not self.update_finished

@mock.patch("subprocess.Popen", new=no_conda)
@mock.patch("subprocess.run", new=no_conda)
@mock.patch("mslib.utils.Worker.create", create_mock)
def test_exception(self):
self.updater.new_version = "999.999.999"
self.updater.old_version = "999.999.999"
self.updater.update_mss()
assert self.status == "Update failed, please do it manually."
assert not self.update_finished

@mock.patch("subprocess.Popen", new=SubprocessSameMock)
@mock.patch("subprocess.run", new=SubprocessSameMock)
@mock.patch("PyQt5.QtWidgets.QMessageBox.information", return_value=QtWidgets.QMessageBox.Yes)
@mock.patch("mslib.utils.Worker.create", create_mock)
def test_ui(self, mock):
ui = UpdaterUI()
ui.updater.on_update_available.emit("", "")
QtTest.QTest.qWait(100)
assert ui.statusLabel.text() == "Update successful. Please restart MSS."
assert ui.btRestart.isEnabled()
19 changes: 18 additions & 1 deletion mslib/msui/mss_pyui.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@
from mslib.msui import constants
from mslib.msui import wms_control
from mslib.msui import mscolab
from mslib.utils import config_loader, setup_logging
from mslib.msui.updater import UpdaterUI
from mslib.utils import config_loader, setup_logging, Worker, Updater
from mslib.plugins.io.csv import load_from_csv, save_to_csv
from mslib.msui.icons import icons, python_powered
from mslib.msui.mss_qt import get_open_filename, get_save_filename
Expand Down Expand Up @@ -278,6 +279,11 @@ def __init__(self, *args):
# Status Bar
self.labelStatusbar.setText(self.status())

# Don't start the updater during a test run of mss_pyui
if "pytest" not in sys.modules:
self.updater = UpdaterUI(self)
self.actionUpdater.triggered.connect(self.updater.show)

@staticmethod
def preload_wms(urls):
"""
Expand Down Expand Up @@ -778,6 +784,7 @@ def main():
default=os.path.join(constants.MSS_CONFIG_PATH, "mss_pyui.log"))
parser.add_argument("-m", "--menu", help="adds mss to menu", action="store_true", default=False)
parser.add_argument("-d", "--deinstall", help="removes mss from menu", action="store_true", default=False)
parser.add_argument("--update", help="Updates MSS to the newest version", action="store_true", default=False)

args = parser.parse_args()

Expand All @@ -789,6 +796,16 @@ def main():
print("Version:", __version__)
sys.exit()

if args.update:
updater = Updater()
updater.on_update_available.connect(lambda old, new: updater.update_mss())
updater.on_log_update.connect(lambda s: print(s.replace("\n", "")))
updater.on_status_update.connect(lambda s: print(s.replace("\n", "")))
updater.run()
while Worker.workers:
list(Worker.workers)[0].wait()
sys.exit()

setup_logging(args)

if args.menu:
Expand Down
1 change: 1 addition & 0 deletions mslib/msui/mss_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ def variant_to_float(variant, locale=QtCore.QLocale()):
for mod in [
"ui_about_dialog",
"ui_shortcuts",
"ui_updater_dialog",
"ui_hexagon_dockwidget",
"ui_kmloverlay_dockwidget",
"ui_customize_kml",
Expand Down
5 changes: 5 additions & 0 deletions mslib/msui/qt5/ui_mainwindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ def setupUi(self, MSSMainWindow):
self.actionShortcuts = QtWidgets.QAction(MSSMainWindow)
self.actionShortcuts.setShortcutContext(QtCore.Qt.ApplicationShortcut)
self.actionShortcuts.setObjectName("actionShortcuts")
self.actionUpdater = QtWidgets.QAction(MSSMainWindow)
self.actionUpdater.setObjectName("actionUpdater")
self.menu_File.addAction(self.actionNewFlightTrack)
self.menu_File.addAction(self.actionOpenFlightTrack)
self.menu_File.addSeparator()
Expand All @@ -122,6 +124,7 @@ def setupUi(self, MSSMainWindow):
self.menu_Help.addAction(self.actionOnlineHelp)
self.menu_Help.addAction(self.actionAboutMSUI)
self.menu_Help.addAction(self.actionShortcuts)
self.menu_Help.addAction(self.actionUpdater)
self.menu_Mscolab.addAction(self.actionMscolabProjects)
self.menubar.addAction(self.menu_File.menuAction())
self.menubar.addAction(self.menu_View.menuAction())
Expand Down Expand Up @@ -178,3 +181,5 @@ def retranslateUi(self, MSSMainWindow):
self.actionShortcuts.setText(_translate("MSSMainWindow", "Shortcuts"))
self.actionShortcuts.setToolTip(_translate("MSSMainWindow", "Show Current Shortcuts"))
self.actionShortcuts.setShortcut(_translate("MSSMainWindow", "Alt+S"))
self.actionUpdater.setText(_translate("MSSMainWindow", "Updater"))
self.actionUpdater.setToolTip(_translate("MSSMainWindow", "Open the Updater Dialog"))
65 changes: 65 additions & 0 deletions mslib/msui/qt5/ui_updater_dialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'mslib/msui/ui/ui_updater_dialog.ui'
#
# Created by: PyQt5 UI code generator 5.12.3
#
# WARNING! All changes made in this file will be lost!


from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_Updater(object):
def setupUi(self, Updater):
Updater.setObjectName("Updater")
Updater.setWindowModality(QtCore.Qt.NonModal)
Updater.resize(854, 338)
self.verticalLayout = QtWidgets.QVBoxLayout(Updater)
self.verticalLayout.setObjectName("verticalLayout")
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.labelVersion = QtWidgets.QLabel(Updater)
self.labelVersion.setObjectName("labelVersion")
self.horizontalLayout.addWidget(self.labelVersion)
self.btUpdate = QtWidgets.QPushButton(Updater)
self.btUpdate.setEnabled(False)
self.btUpdate.setObjectName("btUpdate")
self.horizontalLayout.addWidget(self.btUpdate)
self.btRestart = QtWidgets.QPushButton(Updater)
self.btRestart.setEnabled(False)
self.btRestart.setObjectName("btRestart")
self.horizontalLayout.addWidget(self.btRestart)
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout.addItem(spacerItem)
self.label = QtWidgets.QLabel(Updater)
self.label.setOpenExternalLinks(True)
self.label.setObjectName("label")
self.horizontalLayout.addWidget(self.label)
self.verticalLayout.addLayout(self.horizontalLayout)
self.statusLabel = QtWidgets.QLabel(Updater)
self.statusLabel.setObjectName("statusLabel")
self.verticalLayout.addWidget(self.statusLabel)
self.output = QtWidgets.QPlainTextEdit(Updater)
font = QtGui.QFont()
font.setFamily("Sans Serif")
font.setStyleStrategy(QtGui.QFont.PreferDefault)
self.output.setFont(font)
self.output.setLineWrapMode(QtWidgets.QPlainTextEdit.NoWrap)
self.output.setReadOnly(True)
self.output.setPlainText("")
self.output.setCenterOnScroll(False)
self.output.setObjectName("output")
self.verticalLayout.addWidget(self.output)

self.retranslateUi(Updater)
QtCore.QMetaObject.connectSlotsByName(Updater)

def retranslateUi(self, Updater):
_translate = QtCore.QCoreApplication.translate
Updater.setWindowTitle(_translate("Updater", "Updater"))
self.labelVersion.setText(_translate("Updater", "Newest Version: x.x.x"))
self.btUpdate.setText(_translate("Updater", "Update"))
self.btRestart.setText(_translate("Updater", "Restart MSS"))
self.label.setText(_translate("Updater", "<html><head/><body><p><a href=\"https://mss.readthedocs.io/en/stable/installation.html#install\"><span style=\" text-decoration: underline; color:#0000ff;\">Manual update instructions</span></a></p></body></html>"))
self.statusLabel.setText(_translate("Updater", "Nothing to do"))
10 changes: 8 additions & 2 deletions mslib/msui/ui/ui_mainwindow.ui
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ Save a flight track to name it.</string>
<addaction name="actionOnlineHelp"/>
<addaction name="actionAboutMSUI"/>
<addaction name="actionShortcuts"/>
<addaction name="actionUpdater"/>
</widget>
<widget class="QMenu" name="menu_Mscolab">
<property name="title">
Expand Down Expand Up @@ -277,8 +278,13 @@ Save a flight track to name it.</string>
<property name="shortcut">
<string>Alt+S</string>
</property>
<property name="shortcutContext">
<enum>Qt::ApplicationShortcut</enum>
</action>
<action name="actionUpdater">
<property name="text">
<string>Updater</string>
</property>
<property name="toolTip">
<string>Open the Updater Dialog</string>
</property>
</action>
</widget>
Expand Down
Loading

0 comments on commit 890ef74

Please sign in to comment.