mirror of https://gitlab.com/openlp/openlp.git
Merge branch 'future_proofing_downgrades' into 'master'
Future proof downgrades See merge request openlp/openlp!321
This commit is contained in:
commit
2e56c07639
|
@ -32,7 +32,7 @@ import sys
|
|||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from shutil import copytree
|
||||
from shutil import copytree, move
|
||||
from traceback import format_exception
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWebEngineWidgets, QtWidgets # noqa
|
||||
|
@ -311,6 +311,71 @@ def set_up_logging(log_path):
|
|||
print(f'Logging to: {file_path} and level {log.level}')
|
||||
|
||||
|
||||
def backup_if_version_changed(settings):
|
||||
"""
|
||||
Check version of settings and the application version and backup if the version is different.
|
||||
Returns true if a backup was not required or the backup succeeded,
|
||||
false if backup required but was cancelled or failed.
|
||||
|
||||
:param Settings settings: The settings object
|
||||
:rtype: bool
|
||||
"""
|
||||
is_downgrade = get_version()['version'] < settings.value('core/application version')
|
||||
# No need to backup if version matches and we're not downgrading
|
||||
if not (settings.version_mismatched() and settings.value('core/has run wizard')) and not is_downgrade:
|
||||
return True
|
||||
now = datetime.now()
|
||||
data_folder_path = AppLocation.get_data_path()
|
||||
timestamp = time.strftime("%Y%m%d-%H%M%S")
|
||||
data_folder_backup_path = data_folder_path.with_name(data_folder_path.name + '-' + timestamp)
|
||||
# Warning if OpenLP is downgrading
|
||||
if is_downgrade:
|
||||
close_result = QtWidgets.QMessageBox.warning(
|
||||
None, translate('OpenLP', 'Downgrade'),
|
||||
translate('OpenLP', 'OpenLP has found a configuration file created by a newer version of OpenLP. '
|
||||
'OpenLP will start with a fresh install as downgrading data is not supported. Any existing data '
|
||||
'will be backed up to:\n\n{data_folder_backup_path}\n\n'
|
||||
'Do you want to continue?').format(data_folder_backup_path=data_folder_backup_path),
|
||||
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No),
|
||||
QtWidgets.QMessageBox.No)
|
||||
if close_result == QtWidgets.QMessageBox.No:
|
||||
# Return false as backup failed.
|
||||
return False
|
||||
# Backup the settings
|
||||
if settings.version_mismatched() or is_downgrade:
|
||||
settings_back_up_path = data_folder_path / (now.strftime('%Y-%m-%d %H-%M') + '.conf')
|
||||
log.info(f'Settings are being backed up to {settings_back_up_path}')
|
||||
if not is_downgrade:
|
||||
# Inform user of settings backup location
|
||||
QtWidgets.QMessageBox.information(
|
||||
None, translate('OpenLP', 'Settings Backup'),
|
||||
translate('OpenLP', 'Your settings are about to be upgraded. A backup will be created at '
|
||||
'{settings_back_up_path}').format(settings_back_up_path=settings_back_up_path))
|
||||
# Backup the settings
|
||||
try:
|
||||
settings.export(settings_back_up_path)
|
||||
except OSError:
|
||||
QtWidgets.QMessageBox.warning(
|
||||
None, translate('OpenLP', 'Settings Backup'),
|
||||
translate('OpenLP', 'Settings back up failed.\n\nOpenLP will attempt to continue.'))
|
||||
# Backup and remove data folder if downgrading
|
||||
if is_downgrade:
|
||||
log.info(f'Data folder being backed up to {data_folder_backup_path}')
|
||||
try:
|
||||
# We don't want to use data from newer versions, so rather than a copy, we'll just move/rename
|
||||
move(data_folder_path, data_folder_backup_path)
|
||||
except OSError:
|
||||
log.exception('Failed to backup data for downgrade')
|
||||
QtWidgets.QMessageBox.critical(None, translate('OpenLP', 'OpenLP Backup'),
|
||||
translate('OpenLP', 'Backup of the data folder failed during downgrade.'))
|
||||
return False
|
||||
# Reset all the settings if we're downgrading
|
||||
if is_downgrade:
|
||||
settings.clear()
|
||||
settings.upgrade_settings()
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
The main function which parses command line options and then runs
|
||||
|
@ -359,11 +424,11 @@ def main():
|
|||
log.info('Running portable')
|
||||
portable_settings_path = data_path / 'OpenLP.ini'
|
||||
# Make this our settings file
|
||||
log.info('INI file: {name}'.format(name=portable_settings_path))
|
||||
log.info(f'INI file: {portable_settings_path}')
|
||||
Settings.set_filename(portable_settings_path)
|
||||
portable_settings = Settings()
|
||||
# Set our data path
|
||||
log.info('Data path: {name}'.format(name=data_path))
|
||||
log.info(f'Data path: {data_path}')
|
||||
# Point to our data path
|
||||
portable_settings.setValue('advanced/data path', data_path)
|
||||
portable_settings.setValue('advanced/is portable', True)
|
||||
|
@ -406,24 +471,11 @@ def main():
|
|||
if app.is_data_path_missing():
|
||||
server.close_server()
|
||||
sys.exit()
|
||||
if settings.can_upgrade():
|
||||
now = datetime.now()
|
||||
# Only back up if OpenLP has previously run.
|
||||
if settings.value('core/has run wizard'):
|
||||
back_up_path = AppLocation.get_data_path() / (now.strftime('%Y-%m-%d %H-%M') + '.conf')
|
||||
log.info('Settings about to be upgraded. Existing settings are being backed up to {back_up_path}'
|
||||
.format(back_up_path=back_up_path))
|
||||
QtWidgets.QMessageBox.information(
|
||||
None, translate('OpenLP', 'Settings Upgrade'),
|
||||
translate('OpenLP', 'Your settings are about to be upgraded. A backup will be created at '
|
||||
'{back_up_path}').format(back_up_path=back_up_path))
|
||||
try:
|
||||
settings.export(back_up_path)
|
||||
except OSError:
|
||||
QtWidgets.QMessageBox.warning(
|
||||
None, translate('OpenLP', 'Settings Upgrade'),
|
||||
translate('OpenLP', 'Settings back up failed.\n\nContinuing to upgrade.'))
|
||||
settings.upgrade_settings()
|
||||
# Do a backup
|
||||
if not backup_if_version_changed(settings):
|
||||
# Backup failed, stop before we damage data.
|
||||
server.close_server()
|
||||
sys.exit()
|
||||
# First time checks in settings
|
||||
if not settings.value('core/has run wizard'):
|
||||
if not FirstTimeLanguageForm().exec():
|
||||
|
|
|
@ -653,9 +653,17 @@ class Settings(QtCore.QSettings):
|
|||
key = self.group() + '/' + key
|
||||
return Settings.__default_settings__[key]
|
||||
|
||||
def can_upgrade(self):
|
||||
def from_future(self):
|
||||
"""
|
||||
Can / should the settings be upgraded
|
||||
Is the settings version higher then the version required by OpenLP
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return __version__ < self.value('settings/version')
|
||||
|
||||
def version_mismatched(self):
|
||||
"""
|
||||
Are the settings a different version as required by OpenLP
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
|
|
|
@ -884,8 +884,17 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow, LogMixin, RegistryPropert
|
|||
|
||||
self.log_info('hook upgrade_plugin_settings')
|
||||
self.plugin_manager.hook_upgrade_plugin_settings(import_settings)
|
||||
# If settings are from the future, we can't import.
|
||||
if import_settings.from_future():
|
||||
QtWidgets.QMessageBox.critical(self, translate('OpenLP.MainWindow', 'Import settings'),
|
||||
translate('OpenLP.MainWindow', 'OpenLP cannot import settings '
|
||||
'from a newer version of OpenLP.\n\n'
|
||||
'Processing has terminated and '
|
||||
'no changes have been made.'),
|
||||
QtWidgets.QMessageBox.StandardButtons(QtWidgets.QMessageBox.Ok))
|
||||
return
|
||||
# Upgrade settings to prepare the import.
|
||||
if import_settings.can_upgrade():
|
||||
if import_settings.version_mismatched():
|
||||
import_settings.upgrade_settings()
|
||||
# Lets do a basic sanity check. If it contains this string we can assume it was created by OpenLP and so we'll
|
||||
# load what we can from it, and just silently ignore anything we don't recognise.
|
||||
|
|
|
@ -237,13 +237,25 @@ def test_upgrade_multiple_one_invalid(mocked_remove, mocked_setValue, mocked_val
|
|||
assert mocked_contains.call_args_list == [call('multiple/value 1'), call('multiple/value 2')]
|
||||
|
||||
|
||||
def test_can_upgrade():
|
||||
"""Test the Settings.can_upgrade() method"""
|
||||
def test_from_future(settings):
|
||||
"""Test the Settings.from_future() method"""
|
||||
# GIVEN: A Settings object
|
||||
settings.setValue('settings/version', 100)
|
||||
|
||||
# WHEN: from_future() is called
|
||||
result = settings.from_future()
|
||||
|
||||
# THEN: The result should be true
|
||||
assert result is True, 'The settings should be detected as a newer version'
|
||||
|
||||
|
||||
def test_version_mismatched():
|
||||
"""Test the Settings.version_mismatched() method"""
|
||||
# GIVEN: A Settings object
|
||||
local_settings = Settings()
|
||||
|
||||
# WHEN: can_upgrade() is run
|
||||
result = local_settings.can_upgrade()
|
||||
# WHEN: version_mismatched() is run
|
||||
result = local_settings.version_mismatched()
|
||||
|
||||
# THEN: The result should be True
|
||||
assert result is True, 'The settings should be upgradeable'
|
||||
|
|
|
@ -19,6 +19,10 @@
|
|||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
##########################################################################
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
from pathlib import Path
|
||||
from tempfile import mkdtemp
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
@ -27,10 +31,38 @@ from PyQt5 import QtCore, QtWidgets
|
|||
# Mock QtWebEngineWidgets
|
||||
sys.modules['PyQt5.QtWebEngineWidgets'] = MagicMock()
|
||||
|
||||
from openlp.core.app import parse_options
|
||||
from openlp.core.app import parse_options, backup_if_version_changed, main as app_main
|
||||
from openlp.core.common import is_win
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app_main_env():
|
||||
with patch('openlp.core.app.Settings') as mock_settings, \
|
||||
patch('openlp.core.app.Registry') as mock_registry, \
|
||||
patch('openlp.core.app.AppLocation') as mock_apploc, \
|
||||
patch('openlp.core.app.LanguageManager'), \
|
||||
patch('openlp.core.app.qInitResources'), \
|
||||
patch('openlp.core.app.parse_options'), \
|
||||
patch('openlp.core.app.QtWidgets.QApplication'), \
|
||||
patch('openlp.core.app.QtWidgets.QMessageBox.warning') as mock_warn, \
|
||||
patch('openlp.core.app.QtWidgets.QMessageBox.information'), \
|
||||
patch('openlp.core.app.OpenLP') as mock_openlp, \
|
||||
patch('openlp.core.app.Server') as mock_server, \
|
||||
patch('openlp.core.app.sys'):
|
||||
mock_registry.return_value = MagicMock()
|
||||
mock_settings.return_value = MagicMock()
|
||||
openlp_server = MagicMock()
|
||||
mock_server.return_value = openlp_server
|
||||
openlp_server.is_another_instance_running.return_value = False
|
||||
mock_apploc.get_data_path.return_value = Path()
|
||||
mock_apploc.get_directory.return_value = Path()
|
||||
mock_warn.return_value = True
|
||||
openlp_instance = MagicMock()
|
||||
mock_openlp.return_value = openlp_instance
|
||||
openlp_instance.is_data_path_missing.return_value = False
|
||||
yield
|
||||
|
||||
|
||||
def test_parse_options_basic():
|
||||
"""
|
||||
Test the parse options process works
|
||||
|
@ -269,3 +301,57 @@ def test_backup_on_upgrade(mocked_question, mocked_get_version, qapp, settings):
|
|||
assert mocked_question.call_count == 1, 'A question should have been asked!'
|
||||
qapp.splash.hide.assert_called_once_with()
|
||||
qapp.splash.show.assert_called_once_with()
|
||||
|
||||
|
||||
@patch('openlp.core.app.OpenLP')
|
||||
@patch('openlp.core.app.sys')
|
||||
@patch('openlp.core.app.backup_if_version_changed')
|
||||
def test_main(mock_backup, mock_sys, mock_openlp, app_main_env):
|
||||
"""
|
||||
Test the main method performs primary actions
|
||||
"""
|
||||
# GIVEN: A mocked openlp instance
|
||||
openlp_instance = MagicMock()
|
||||
mock_openlp.return_value = openlp_instance
|
||||
openlp_instance.is_data_path_missing.return_value = False
|
||||
mock_backup.return_value = True
|
||||
|
||||
# WHEN: the main method is run
|
||||
app_main()
|
||||
|
||||
# THEN: Check the application is run and exited
|
||||
openlp_instance.run.assert_called_once()
|
||||
mock_sys.exit.assert_called_once()
|
||||
|
||||
|
||||
@patch('openlp.core.app.QtWidgets.QMessageBox.warning')
|
||||
@patch('openlp.core.app.get_version')
|
||||
@patch('openlp.core.app.AppLocation.get_data_path')
|
||||
@patch('openlp.core.app.move')
|
||||
def test_main_future_settings(mock_move, mock_get_path, mock_version, mock_warn, app_main_env, settings):
|
||||
"""
|
||||
Test the backup_if_version_changed method backs up data if version from the future and user consents
|
||||
"""
|
||||
# GIVEN: A mocked openlp instance with mocked future settings
|
||||
settings.from_future = MagicMock(return_value=True)
|
||||
settings.version_mismatched = MagicMock(return_value=True)
|
||||
settings.clear = MagicMock()
|
||||
settings.setValue('core/application version', '3.0.1')
|
||||
mock_warn.return_value = QtWidgets.QMessageBox.Yes
|
||||
MOCKED_VERSION = {
|
||||
'full': '2.9.3',
|
||||
'version': '2.9.3',
|
||||
'build': 'None'
|
||||
}
|
||||
mock_version.return_value = MOCKED_VERSION
|
||||
temp_folder = Path(mkdtemp())
|
||||
mock_get_path.return_value = temp_folder
|
||||
|
||||
# WHEN: the main method is run
|
||||
result = backup_if_version_changed(settings)
|
||||
|
||||
# THEN: Check everything was backed up, the settings were cleared and the warn prompt was shown
|
||||
assert result is True
|
||||
mock_move.assert_called_once()
|
||||
settings.clear.assert_called_once_with()
|
||||
mock_warn.assert_called_once()
|
||||
|
|
|
@ -730,3 +730,27 @@ def test_screen_changed_modal_sets_timestamp_before_blocking_on_modal(mocked_war
|
|||
# the blocking modal is shown.
|
||||
mocked_warning.assert_called_once()
|
||||
assert main_window.screen_change_timestamp is None
|
||||
|
||||
|
||||
@patch('openlp.core.ui.mainwindow.QtWidgets.QMessageBox.critical')
|
||||
@patch('openlp.core.ui.mainwindow.FileDialog')
|
||||
@patch('openlp.core.ui.mainwindow.shutil')
|
||||
@patch('openlp.core.ui.mainwindow.Settings')
|
||||
def test_on_settings_import_item_clicked(mock_settings, mock_shutil, mock_dialog, mock_crit, main_window_reduced):
|
||||
"""
|
||||
Check we don't attempt to import incompatible settings from the future
|
||||
"""
|
||||
# GIVEN: a
|
||||
settings_instance = MagicMock()
|
||||
mock_settings.return_value = settings_instance
|
||||
mock_dialog.getOpenFileName.return_value = [MagicMock(name='bob'), '']
|
||||
settings_instance.from_future.return_value = True
|
||||
Registry().register('plugin_manager', MagicMock())
|
||||
mock_crit.return_value = True
|
||||
|
||||
# WHEN: the function is called
|
||||
main_window_reduced.on_settings_import_item_clicked()
|
||||
|
||||
# THEN: The from_future should have been checked, but code should not have started to copy values
|
||||
settings_instance.from_future.assert_called_once_with()
|
||||
settings_instance.value.assert_not_called()
|
||||
|
|
Loading…
Reference in New Issue