Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions test_preferences_dirty_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""Hermetic characterization for #603 v3 broad Preferences dirty state."""
import os
import sys
import types
import importlib.util

BASE = os.path.join(os.path.dirname(__file__), "usr/lib/linuxmint/mintUpdate")
sys.path.insert(0, BASE)

def load(name, filename):
spec = importlib.util.spec_from_file_location(name, os.path.join(BASE, filename))
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod

prefs_mod = load("prefs_v3", "preferences.py")
update_mod = load("update_v3", "mintUpdate.py")

class Settings:
def __init__(self): self.handlers = {}; self.next_id = 0
def connect(self, signal, callback):
self.next_id += 1; self.handlers[self.next_id] = callback; return self.next_id
def disconnect(self, ident): self.handlers.pop(ident)
def emit(self, key):
for callback in list(self.handlers.values()): callback(self, key)

class Win:
def __init__(self, dirty=False): self.preferences_changed = dirty

# GSettings mutation marks dirty; no signal stays clean.
p = prefs_mod.PreferencesWindow.__new__(prefs_mod.PreferencesWindow)
p.preferences_changed = False
p.settings = Settings()
p._settings_changed_handler = p.settings.connect("changed", p._on_settings_changed)
p.settings.emit("tracker-max-days")
assert p.preferences_changed is True
p._on_preferences_destroyed(None)
assert not p.settings.handlers, "destroy must disconnect session handler"

p2 = prefs_mod.PreferencesWindow.__new__(prefs_mod.PreferencesWindow)
p2.preferences_changed = False
p2.settings = Settings()
p2._settings_changed_handler = p2.settings.connect("changed", p2._on_settings_changed)
assert p2.preferences_changed is False, "no-op stays clean"

# Close contract: no-op -> none, dirty -> one refresh, restart wins.
app = update_mod.MintUpdate.__new__(update_mod.MintUpdate)
app.ui_window = types.SimpleNamespace(set_sensitive=lambda _: None)
app.preferences_window = object()
app.app_restart_required = False
calls = []
app.refresh = lambda force: calls.append(("refresh", force))
app.restart_app = lambda: calls.append(("restart",))
app._on_preferences_destroyed(Win(False))
assert calls == [], calls
app.preferences_window = object()
app._on_preferences_destroyed(Win(True))
assert calls == [("refresh", False)], calls
app.preferences_window = object()
app.app_restart_required = True
app._on_preferences_destroyed(Win(True))
assert calls == [("refresh", False), ("restart",)], calls

# Automation: only a successful state transition dirties.
class Toggle:
def __init__(self, active): self.active = active
def get_active(self): return self.active
def set_active(self, v): self.active = v

old_isfile, old_run = prefs_mod.os.path.isfile, prefs_mod.subprocess.run
try:
prefs_mod.AUTOMATIONS["test"] = (None, None, "/tmp/v3-touch")
state = {"exists": False}
prefs_mod.os.path.isfile = lambda _: state["exists"]
def success(_): state["exists"] = True
prefs_mod.subprocess.run = success
p3 = prefs_mod.PreferencesWindow.__new__(prefs_mod.PreferencesWindow)
p3.preferences_changed = False
p3._toggle_automation(Toggle(True), "test")
assert p3.preferences_changed is True
finally:
prefs_mod.os.path.isfile, prefs_mod.subprocess.run = old_isfile, old_run
prefs_mod.AUTOMATIONS.pop("test", None)

print("V3 HARNESS OK: GSettings broad signal, no-op, destroy gate, restart precedence, automation transition")
3 changes: 2 additions & 1 deletion usr/lib/linuxmint/mintUpdate/mintUpdate.py
Original file line number Diff line number Diff line change
Expand Up @@ -1667,10 +1667,11 @@ def open_preferences(self, widget, show_automation=False):

def _on_preferences_destroyed(self, window):
self.ui_window.set_sensitive(True)
preferences_changed = getattr(window, "preferences_changed", False)
self.preferences_window = None
if self.app_restart_required:
self.restart_app()
else:
elif preferences_changed:
self.refresh(False)

def restart_app(self):
Expand Down
14 changes: 12 additions & 2 deletions usr/lib/linuxmint/mintUpdate/preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class PreferencesWindow:
def __init__(self, parent, show_automation=False):
self.parent = parent
self.settings = parent.settings
self.preferences_changed = False
self._settings_changed_handler = self.settings.connect("changed", self._on_settings_changed)

gladefile = "/usr/share/linuxmint/mintupdate/preferences.ui"
builder = Gtk.Builder()
Expand Down Expand Up @@ -58,6 +60,7 @@ def __init__(self, parent, show_automation=False):
self._build_blacklist(builder)
self._build_automation(builder)

self.window.connect("destroy", self._on_preferences_destroyed)
self.window.show_all()

if show_automation:
Expand Down Expand Up @@ -214,6 +217,12 @@ def _build_automation(self, builder):
for switch in additional_options:
section.add_row(switch)

def _on_settings_changed(self, settings, key):
self.preferences_changed = True

def _on_preferences_destroyed(self, window):
self.settings.disconnect(self._settings_changed_handler)

def _export_blacklist(self, widget):
filename = os.path.join(tempfile.gettempdir(), "mintUpdate/blacklist")
blacklist = self.settings.get_strv("blacklisted-packages")
Expand All @@ -224,8 +233,7 @@ def _export_blacklist(self, widget):
def _set_auto_upgrade(self, widget, param):
self._toggle_automation(widget, "upgrade")

@staticmethod
def _toggle_automation(widget, automation_id):
def _toggle_automation(self, widget, automation_id):
touchfile = AUTOMATIONS[automation_id][2]
exists = os.path.isfile(touchfile)
action = None
Expand All @@ -237,6 +245,8 @@ def _toggle_automation(widget, automation_id):
subprocess.run(["pkexec", "/usr/bin/mintupdate-automation", automation_id, action])
if widget.get_active() != os.path.isfile(touchfile):
widget.set_active(not widget.get_active())
elif action:
self.preferences_changed = True

def _save_blacklist(self, treeview):
blacklist = []
Expand Down