feat(updater): Introduce new settings and rename old ones

This commit is contained in:
Alexis Métaireau 2024-06-12 10:26:54 +02:00
parent 3ba9181888
commit 4a12853f46
No known key found for this signature in database
GPG key ID: C65C7A89A8FFC56E
5 changed files with 79 additions and 48 deletions

View file

@ -107,7 +107,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.toggle_updates_action.triggered.connect(self.toggle_updates_triggered) self.toggle_updates_action.triggered.connect(self.toggle_updates_triggered)
self.toggle_updates_action.setCheckable(True) self.toggle_updates_action.setCheckable(True)
self.toggle_updates_action.setChecked( self.toggle_updates_action.setChecked(
bool(self.dangerzone.settings.get("updater_check")) bool(self.dangerzone.settings.get("updater_release_check"))
) )
# Add the "Exit" action # Add the "Exit" action
@ -184,8 +184,8 @@ class MainWindow(QtWidgets.QMainWindow):
def show_update_success(self) -> None: def show_update_success(self) -> None:
"""Inform the user about a new Dangerzone release.""" """Inform the user about a new Dangerzone release."""
version = self.dangerzone.settings.get("updater_latest_version") version = self.dangerzone.settings.get("updater_release_latest_version")
changelog = self.dangerzone.settings.get("updater_latest_changelog") changelog = self.dangerzone.settings.get("updater_release_latest_changelog")
changelog_widget = CollapsibleBox("What's New?") changelog_widget = CollapsibleBox("What's New?")
changelog_layout = QtWidgets.QVBoxLayout() changelog_layout = QtWidgets.QVBoxLayout()
@ -230,7 +230,7 @@ class MainWindow(QtWidgets.QMainWindow):
def toggle_updates_triggered(self) -> None: def toggle_updates_triggered(self) -> None:
"""Change the underlying update check settings based on the user's choice.""" """Change the underlying update check settings based on the user's choice."""
check = self.toggle_updates_action.isChecked() check = self.toggle_updates_action.isChecked()
self.dangerzone.settings.set("updater_check", check) self.dangerzone.settings.set("updater_release_check", check)
self.dangerzone.settings.save() self.dangerzone.settings.save()
def handle_updates(self, report: UpdateReport) -> None: def handle_updates(self, report: UpdateReport) -> None:
@ -274,8 +274,12 @@ class MainWindow(QtWidgets.QMainWindow):
hamburger_menu.insertAction(sep, error_action) hamburger_menu.insertAction(sep, error_action)
else: else:
log.debug(f"Handling new version: {report.version}") log.debug(f"Handling new version: {report.version}")
self.dangerzone.settings.set("updater_latest_version", report.version) self.dangerzone.settings.set(
self.dangerzone.settings.set("updater_latest_changelog", report.changelog) "updater_release_latest_version", report.version
)
self.dangerzone.settings.set(
"updater_release_latest_changelog", report.changelog
)
self.dangerzone.settings.set("updater_errors", 0) self.dangerzone.settings.set("updater_errors", 0)
# FIXME: Save the settings to the filesystem only when they have really changed, # FIXME: Save the settings to the filesystem only when they have really changed,

View file

@ -126,11 +126,11 @@ class UpdaterThread(QtCore.QThread):
@property @property
def check(self) -> Optional[bool]: def check(self) -> Optional[bool]:
return self.dangerzone.settings.get("updater_check") return self.dangerzone.settings.get("updater_release_check")
@check.setter @check.setter
def check(self, val: bool) -> None: def check(self, val: bool) -> None:
self.dangerzone.settings.set("updater_check", val, autosave=True) self.dangerzone.settings.set("updater_release_check", val, autosave=True)
def prompt_for_checks(self) -> Optional[bool]: def prompt_for_checks(self) -> Optional[bool]:
"""Ask the user if they want to be informed about Dangerzone updates.""" """Ask the user if they want to be informed about Dangerzone updates."""
@ -256,12 +256,14 @@ class UpdaterThread(QtCore.QThread):
2. In GitHub, by hitting the latest releases API. 2. In GitHub, by hitting the latest releases API.
""" """
log.debug("Checking for Dangerzone updates") log.debug("Checking for Dangerzone updates")
latest_version = self.dangerzone.settings.get("updater_latest_version") latest_version = self.dangerzone.settings.get("updater_release_latest_version")
if version.parse(get_version()) < version.parse(latest_version): if version.parse(get_version()) < version.parse(latest_version):
log.debug("Determined that there is an update due to cached results") log.debug("Determined that there is an update due to cached results")
return UpdateReport( return UpdateReport(
version=latest_version, version=latest_version,
changelog=self.dangerzone.settings.get("updater_latest_changelog"), changelog=self.dangerzone.settings.get(
"updater_release_latest_changelog"
),
) )
# If the previous check happened before the cooldown period expires, do not # If the previous check happened before the cooldown period expires, do not

View file

@ -26,6 +26,8 @@ class Settings:
) )
self.default_settings: Dict[str, Any] = self.generate_default_settings() self.default_settings: Dict[str, Any] = self.generate_default_settings()
self.load() self.load()
self.migrate_settings()
self.save()
@classmethod @classmethod
def generate_default_settings(cls) -> Dict[str, Any]: def generate_default_settings(cls) -> Dict[str, Any]:
@ -37,14 +39,33 @@ class Settings:
"open": True, "open": True,
"open_app": None, "open_app": None,
"safe_extension": SAFE_EXTENSION, "safe_extension": SAFE_EXTENSION,
"updater_check": None,
"updater_last_check": None, # last check in UNIX epoch (secs since 1970)
# FIXME: How to invalidate those if they change upstream? # FIXME: How to invalidate those if they change upstream?
"updater_latest_version": get_version(), "updater_last_check": None, # last check in UNIX epoch (secs since 1970)
"updater_latest_changelog": "",
"updater_errors": 0, "updater_errors": 0,
"updater_release_check": None,
"updater_release_latest_version": get_version(),
"updater_release_latest_changelog": "",
"updater_docker_check": None,
"updater_docker_latest_version": None,
"updater_docker_latest_changelog": "",
} }
def migrate_settings(self):
# Backward compatibility layer for the settings.
name_replacements = (
# Some `updater_*` settings have been replaced with their
# `updater_release_*` counterpart to better differenciate the type of
# updates they are tracking, (as we are also checking for Docker Desktop
# updates).
("updater_check", "updater_release_check"),
("updater_latest_version", "updater_release_latest_version"),
("updater_latest_changelog", "updater_release_latest_changelog"),
)
for old_name, new_name in name_replacements:
if old_name in self.settings:
self.settings.set(new_name, self.settings.pop(old_name))
def get(self, key: str) -> Any: def get(self, key: str) -> Any:
return self.settings[key] return self.settings[key]
@ -75,7 +96,8 @@ class Settings:
for key in self.default_settings: for key in self.default_settings:
if key not in self.settings: if key not in self.settings:
self.settings[key] = self.default_settings[key] self.settings[key] = self.default_settings[key]
elif key == "updater_latest_version": elif key == "updater_release_latest_version":
# Update the version with the current one if needed
if version.parse(get_version()) > version.parse(self.get(key)): if version.parse(get_version()) > version.parse(self.get(key)):
self.set(key, get_version()) self.set(key, get_version())
@ -88,8 +110,6 @@ class Settings:
log.info("Settings file doesn't exist, starting with default") log.info("Settings file doesn't exist, starting with default")
self.settings = self.default_settings self.settings = self.default_settings
self.save()
def save(self) -> None: def save(self) -> None:
os.makedirs(self.dangerzone.appdata_path, exist_ok=True) os.makedirs(self.dangerzone.appdata_path, exist_ok=True)
with open(self.settings_filename, "w") as settings_file: with open(self.settings_filename, "w") as settings_file:

View file

@ -38,7 +38,7 @@ def test_default_menu(
updater: UpdaterThread, updater: UpdaterThread,
) -> None: ) -> None:
"""Check that the default menu entries are in order.""" """Check that the default menu entries are in order."""
updater.dangerzone.settings.set("updater_check", True) updater.dangerzone.settings.set("updater_release_check", True)
window = MainWindow(updater.dangerzone) window = MainWindow(updater.dangerzone)
menu_actions = window.hamburger_button.menu().actions() menu_actions = window.hamburger_button.menu().actions()
@ -56,7 +56,7 @@ def test_default_menu(
toggle_updates_action.trigger() toggle_updates_action.trigger()
assert not toggle_updates_action.isChecked() assert not toggle_updates_action.isChecked()
assert updater.dangerzone.settings.get("updater_check") is False assert updater.dangerzone.settings.get("updater_release_check") is False
def test_no_update( def test_no_update(
@ -69,12 +69,12 @@ def test_no_update(
# Check that when no update is detected, e.g., due to update cooldown, an empty # Check that when no update is detected, e.g., due to update cooldown, an empty
# report is received that does not affect the menu entries. # report is received that does not affect the menu entries.
curtime = int(time.time()) curtime = int(time.time())
updater.dangerzone.settings.set("updater_check", True) updater.dangerzone.settings.set("updater_release_check", True)
updater.dangerzone.settings.set("updater_errors", 9) updater.dangerzone.settings.set("updater_errors", 9)
updater.dangerzone.settings.set("updater_last_check", curtime) updater.dangerzone.settings.set("updater_last_check", curtime)
expected_settings = default_updater_settings() expected_settings = default_updater_settings()
expected_settings["updater_check"] = True expected_settings["updater_release_check"] = True
expected_settings["updater_errors"] = 0 # errors must be cleared expected_settings["updater_errors"] = 0 # errors must be cleared
expected_settings["updater_last_check"] = curtime expected_settings["updater_last_check"] = curtime
@ -107,7 +107,7 @@ def test_update_detected(
) -> None: ) -> None:
"""Test that a newly detected version leads to a notification to the user.""" """Test that a newly detected version leads to a notification to the user."""
qt_updater.dangerzone.settings.set("updater_check", True) qt_updater.dangerzone.settings.set("updater_release_check", True)
qt_updater.dangerzone.settings.set("updater_last_check", 0) qt_updater.dangerzone.settings.set("updater_last_check", 0)
qt_updater.dangerzone.settings.set("updater_errors", 9) qt_updater.dangerzone.settings.set("updater_errors", 9)
@ -138,12 +138,12 @@ def test_update_detected(
# Check that the settings have been updated properly. # Check that the settings have been updated properly.
expected_settings = default_updater_settings() expected_settings = default_updater_settings()
expected_settings["updater_check"] = True expected_settings["updater_release_check"] = True
expected_settings["updater_last_check"] = qt_updater.dangerzone.settings.get( expected_settings["updater_last_check"] = qt_updater.dangerzone.settings.get(
"updater_last_check" "updater_last_check"
) )
expected_settings["updater_latest_version"] = "99.9.9" expected_settings["updater_release_latest_version"] = "99.9.9"
expected_settings["updater_latest_changelog"] = "<p>changelog</p>" expected_settings["updater_release_latest_changelog"] = "<p>changelog</p>"
expected_settings["updater_errors"] = 0 expected_settings["updater_errors"] = 0
assert qt_updater.dangerzone.settings.get_updater_settings() == expected_settings assert qt_updater.dangerzone.settings.get_updater_settings() == expected_settings
@ -219,7 +219,7 @@ def test_update_error(
) -> None: ) -> None:
"""Test that an error during an update check leads to a notification to the user.""" """Test that an error during an update check leads to a notification to the user."""
# Test 1 - Check that the first error does not notify the user. # Test 1 - Check that the first error does not notify the user.
qt_updater.dangerzone.settings.set("updater_check", True) qt_updater.dangerzone.settings.set("updater_release_check", True)
qt_updater.dangerzone.settings.set("updater_last_check", 0) qt_updater.dangerzone.settings.set("updater_last_check", 0)
qt_updater.dangerzone.settings.set("updater_errors", 0) qt_updater.dangerzone.settings.set("updater_errors", 0)
@ -246,7 +246,7 @@ def test_update_error(
# Check that the settings have been updated properly. # Check that the settings have been updated properly.
expected_settings = default_updater_settings() expected_settings = default_updater_settings()
expected_settings["updater_check"] = True expected_settings["updater_release_check"] = True
expected_settings["updater_last_check"] = qt_updater.dangerzone.settings.get( expected_settings["updater_last_check"] = qt_updater.dangerzone.settings.get(
"updater_last_check" "updater_last_check"
) )

View file

@ -69,22 +69,24 @@ def test_post_0_4_2_settings(
) -> None: ) -> None:
"""Check settings of installations post-0.4.2. """Check settings of installations post-0.4.2.
Installations from 0.4.2 onwards will have a "updater_latest_version" field in their Installations from 0.4.2 onwards will have a "updater_release_latest_version" field in their
settings. When these installations get upgraded to a newer version, we must make settings. When these installations get upgraded to a newer version, we must make
sure that this field becomes equal to the new version, so that the user is not sure that this field becomes equal to the new version, so that the user is not
erroneously prompted to a version they already have. erroneously prompted to a version they already have.
""" """
# Store the settings of Dangerzone 0.4.2 to the filesystem. # Store the settings of Dangerzone 0.4.2 to the filesystem.
old_settings = settings.Settings.generate_default_settings() old_settings = settings.Settings.generate_default_settings()
old_settings["updater_latest_version"] = "0.4.2" old_settings["updater_release_latest_version"] = "0.4.2"
save_settings(tmp_path, old_settings) save_settings(tmp_path, old_settings)
# Mimic an upgrade to version 0.4.3, by making Dangerzone report that the current # Mimic an upgrade to version 0.4.3, by making Dangerzone report that the current
# version is 0.4.3. # version is 0.4.3.
expected_settings = default_updater_settings() expected_settings = default_updater_settings()
expected_settings["updater_latest_version"] = "0.4.3" expected_settings["updater_release_latest_version"] = "0.4.3"
monkeypatch.setattr( monkeypatch.setattr(
settings, "get_version", lambda: expected_settings["updater_latest_version"] settings,
"get_version",
lambda: expected_settings["updater_release_latest_version"],
) )
# Ensure that the Settings class will correct the latest version field to 0.4.3. # Ensure that the Settings class will correct the latest version field to 0.4.3.
@ -92,13 +94,14 @@ def test_post_0_4_2_settings(
assert updater.dangerzone.settings.get_updater_settings() == expected_settings assert updater.dangerzone.settings.get_updater_settings() == expected_settings
# Simulate an updater check that found a newer Dangerzone version (e.g., 0.4.4). # Simulate an updater check that found a newer Dangerzone version (e.g., 0.4.4).
expected_settings["updater_latest_version"] = "0.4.4" expected_settings["updater_release_latest_version"] = "0.4.4"
updater.dangerzone.settings.set( updater.dangerzone.settings.set(
"updater_latest_version", expected_settings["updater_latest_version"] "updater_release_latest_version",
expected_settings["updater_release_latest_version"],
) )
updater.dangerzone.settings.save() updater.dangerzone.settings.save()
# Ensure that the Settings class will leave the "updater_latest_version" field # Ensure that the Settings class will leave the "updater_release_latest_version" field
# intact the next time we reload the settings. # intact the next time we reload the settings.
updater.dangerzone.settings.load() updater.dangerzone.settings.load()
assert updater.dangerzone.settings.get_updater_settings() == expected_settings assert updater.dangerzone.settings.get_updater_settings() == expected_settings
@ -108,7 +111,7 @@ def test_post_0_4_2_settings(
def test_linux_no_check(updater: UpdaterThread, monkeypatch: MonkeyPatch) -> None: def test_linux_no_check(updater: UpdaterThread, monkeypatch: MonkeyPatch) -> None:
"""Ensure that Dangerzone on Linux does not make any update check.""" """Ensure that Dangerzone on Linux does not make any update check."""
expected_settings = default_updater_settings() expected_settings = default_updater_settings()
expected_settings["updater_check"] = False expected_settings["updater_release_check"] = False
expected_settings["updater_last_check"] = None expected_settings["updater_last_check"] = None
# XXX: Simulate Dangerzone installed via package manager. # XXX: Simulate Dangerzone installed via package manager.
@ -127,7 +130,7 @@ def test_user_prompts(
# When Dangerzone runs for the first time, users should not be asked to enable # When Dangerzone runs for the first time, users should not be asked to enable
# updates. # updates.
expected_settings = default_updater_settings() expected_settings = default_updater_settings()
expected_settings["updater_check"] = None expected_settings["updater_release_check"] = None
expected_settings["updater_last_check"] = 0 expected_settings["updater_last_check"] = 0
assert updater.should_check_for_updates() is False assert updater.should_check_for_updates() is False
assert updater.dangerzone.settings.get_updater_settings() == expected_settings assert updater.dangerzone.settings.get_updater_settings() == expected_settings
@ -142,14 +145,14 @@ def test_user_prompts(
# Check disabling update checks. # Check disabling update checks.
prompt_mock().launch.return_value = False # type: ignore [attr-defined] prompt_mock().launch.return_value = False # type: ignore [attr-defined]
expected_settings["updater_check"] = False expected_settings["updater_release_check"] = False
assert updater.should_check_for_updates() is False assert updater.should_check_for_updates() is False
assert updater.dangerzone.settings.get_updater_settings() == expected_settings assert updater.dangerzone.settings.get_updater_settings() == expected_settings
# Reset the "updater_check" field and check enabling update checks. # Reset the "updater_release_check" field and check enabling update checks.
updater.dangerzone.settings.set("updater_check", None) updater.dangerzone.settings.set("updater_release_check", None)
prompt_mock().launch.return_value = True # type: ignore [attr-defined] prompt_mock().launch.return_value = True # type: ignore [attr-defined]
expected_settings["updater_check"] = True expected_settings["updater_release_check"] = True
assert updater.should_check_for_updates() is True assert updater.should_check_for_updates() is True
assert updater.dangerzone.settings.get_updater_settings() == expected_settings assert updater.dangerzone.settings.get_updater_settings() == expected_settings
@ -159,7 +162,7 @@ def test_user_prompts(
# checks. # checks.
prompt_mock().side_effect = RuntimeError("Should not be called") # type: ignore [attr-defined] prompt_mock().side_effect = RuntimeError("Should not be called") # type: ignore [attr-defined]
for check in [True, False]: for check in [True, False]:
updater.dangerzone.settings.set("updater_check", check) updater.dangerzone.settings.set("updater_release_check", check)
assert updater.should_check_for_updates() == check assert updater.should_check_for_updates() == check
@ -200,8 +203,10 @@ def test_update_checks(
assert_report_equal(report, UpdateReport(error=error_msg)) assert_report_equal(report, UpdateReport(error=error_msg))
# Test 4 - Check that cached version/changelog info do not trigger an update check. # Test 4 - Check that cached version/changelog info do not trigger an update check.
updater.dangerzone.settings.set("updater_latest_version", "99.9.9") updater.dangerzone.settings.set("updater_release_latest_version", "99.9.9")
updater.dangerzone.settings.set("updater_latest_changelog", "<p>changelog</p>") updater.dangerzone.settings.set(
"updater_release_latest_changelog", "<p>changelog</p>"
)
report = updater.check_for_updates() report = updater.check_for_updates()
assert_report_equal( assert_report_equal(
@ -211,7 +216,7 @@ def test_update_checks(
def test_update_checks_cooldown(updater: UpdaterThread, mocker: MockerFixture) -> None: def test_update_checks_cooldown(updater: UpdaterThread, mocker: MockerFixture) -> None:
"""Make sure Dangerzone only checks for updates every X hours""" """Make sure Dangerzone only checks for updates every X hours"""
updater.dangerzone.settings.set("updater_check", True) updater.dangerzone.settings.set("updater_release_check", True)
updater.dangerzone.settings.set("updater_last_check", 0) updater.dangerzone.settings.set("updater_last_check", 0)
# Mock some functions before the tests start # Mock some functions before the tests start
@ -242,8 +247,8 @@ def test_update_checks_cooldown(updater: UpdaterThread, mocker: MockerFixture) -
curtime += 1 curtime += 1
timestamp_mock.return_value = curtime timestamp_mock.return_value = curtime
requests_mock.side_effect = Exception("failed") # type: ignore [attr-defined] requests_mock.side_effect = Exception("failed") # type: ignore [attr-defined]
updater.dangerzone.settings.set("updater_latest_version", get_version()) updater.dangerzone.settings.set("updater_release_latest_version", get_version())
updater.dangerzone.settings.set("updater_latest_changelog", None) updater.dangerzone.settings.set("updater_release_latest_changelog", None)
report = updater.check_for_updates() report = updater.check_for_updates()
assert cooldown_spy.spy_return is True assert cooldown_spy.spy_return is True
@ -264,8 +269,8 @@ def test_update_checks_cooldown(updater: UpdaterThread, mocker: MockerFixture) -
# Test 4: Make Dangerzone check for updates again, but this time, it should # Test 4: Make Dangerzone check for updates again, but this time, it should
# encounter an error while doing so. In that case, the last check timestamp # encounter an error while doing so. In that case, the last check timestamp
# should be bumped, so that subsequent checks don't take place. # should be bumped, so that subsequent checks don't take place.
updater.dangerzone.settings.set("updater_latest_version", get_version()) updater.dangerzone.settings.set("updater_release_latest_version", get_version())
updater.dangerzone.settings.set("updater_latest_changelog", None) updater.dangerzone.settings.set("updater_release_latest_changelog", None)
curtime += updater_module.UPDATE_CHECK_COOLDOWN_SECS curtime += updater_module.UPDATE_CHECK_COOLDOWN_SECS
timestamp_mock.return_value = curtime timestamp_mock.return_value = curtime