convert to markdown (#1)

Reviewed-on: #1
This commit is contained in:
Miguel Jacq 2025-11-08 00:30:46 -06:00
parent 31604a0cd2
commit 39576ac7f3
54 changed files with 1616 additions and 4012 deletions

View file

@ -1,296 +1,180 @@
from pathlib import Path
from PySide6.QtWidgets import QDialog, QFileDialog, QMessageBox, QWidget
from bouquin.db import DBConfig
import pytest
from bouquin.settings_dialog import SettingsDialog
from bouquin.theme import Theme
from bouquin.theme import ThemeManager, ThemeConfig, Theme
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QApplication, QMessageBox, QWidget
class _ThemeSpy:
def __init__(self):
self.calls = []
@pytest.mark.gui
def test_settings_dialog_config_roundtrip(qtbot, tmp_db_cfg, fresh_db, tmp_path):
# Provide a parent that exposes a real ThemeManager (dialog calls parent().themes.set(...))
app = QApplication.instance()
parent = QWidget()
parent.themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
dlg = SettingsDialog(tmp_db_cfg, fresh_db, parent=parent)
qtbot.addWidget(dlg)
dlg.show()
def set(self, t):
self.calls.append(t)
dlg.path_edit.setText(str(tmp_path / "alt.db"))
dlg.idle_spin.setValue(3)
dlg.theme_light.setChecked(True)
dlg.move_todos.setChecked(True)
# Auto-accept the modal QMessageBox that _compact_btn_clicked() shows
def _auto_accept_msgbox():
for w in QApplication.topLevelWidgets():
if isinstance(w, QMessageBox):
w.accept()
QTimer.singleShot(0, _auto_accept_msgbox)
dlg._compact_btn_clicked()
qtbot.wait(50)
dlg._save()
cfg = dlg.config
assert cfg.path.name == "alt.db"
assert cfg.idle_minutes == 3
assert cfg.theme in ("light", "dark", "system")
class _Parent(QWidget):
def __init__(self):
super().__init__()
self.themes = _ThemeSpy()
def test_save_key_toggle_roundtrip(qtbot, tmp_db_cfg, fresh_db, app):
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QApplication, QMessageBox
from bouquin.key_prompt import KeyPrompt
from bouquin.theme import ThemeManager, ThemeConfig, Theme
from PySide6.QtWidgets import QWidget
parent = QWidget()
parent.themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
dlg = SettingsDialog(tmp_db_cfg, fresh_db, parent=parent)
qtbot.addWidget(dlg)
dlg.show()
# Ensure a clean starting state (suite may leave settings toggled on)
dlg.save_key_btn.setChecked(False)
dlg.key = ""
# Robust popup pump so we never miss late dialogs
def _pump():
for w in QApplication.topLevelWidgets():
if isinstance(w, KeyPrompt):
w.edit.setText("supersecret")
w.accept()
elif isinstance(w, QMessageBox):
w.accept()
timer = QTimer()
timer.setInterval(10)
timer.timeout.connect(_pump)
timer.start()
try:
dlg.save_key_btn.setChecked(True)
qtbot.waitUntil(lambda: dlg.key == "supersecret", timeout=1000)
assert dlg.save_key_btn.isChecked()
dlg.save_key_btn.setChecked(False)
qtbot.waitUntil(lambda: dlg.key == "", timeout=1000)
assert dlg.key == ""
finally:
timer.stop()
class FakeDB:
def __init__(self):
self.rekey_called_with = None
self.compact_called = False
self.fail_compact = False
def test_change_key_mismatch_shows_error(qtbot, tmp_db_cfg, tmp_path, app):
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QApplication, QMessageBox, QWidget
from bouquin.key_prompt import KeyPrompt
from bouquin.db import DBManager, DBConfig
from bouquin.theme import ThemeManager, ThemeConfig, Theme
def rekey(self, key: str):
self.rekey_called_with = key
cfg = DBConfig(
path=tmp_path / "iso.db",
key="oldkey",
idle_minutes=0,
theme="light",
move_todos=True,
)
db = DBManager(cfg)
assert db.connect()
db.save_new_version("2000-01-01", "seed", "seed")
def compact(self):
if self.fail_compact:
raise RuntimeError("boom")
self.compact_called = True
class AcceptingPrompt:
def __init__(self, parent=None, title="", message=""):
self._key = ""
self._accepted = True
def set_key(self, k: str):
self._key = k
return self
def exec(self):
return QDialog.Accepted if self._accepted else QDialog.Rejected
def key(self):
return self._key
class RejectingPrompt(AcceptingPrompt):
def __init__(self, *a, **k):
super().__init__()
self._accepted = False
def test_save_persists_all_fields(monkeypatch, qtbot, tmp_path):
db = FakeDB()
cfg = DBConfig(path=tmp_path / "db.sqlite", key="", idle_minutes=15)
saved = {}
def fake_save(cfg2):
saved["cfg"] = cfg2
monkeypatch.setattr("bouquin.settings_dialog.save_db_config", fake_save)
# Drive the "remember key" checkbox via the prompt (no pre-set key)
p = AcceptingPrompt().set_key("sekrit")
monkeypatch.setattr("bouquin.settings_dialog.KeyPrompt", lambda *a, **k: p)
# Provide a lightweight parent that mimics MainWindows `themes` API
class _ThemeSpy:
def __init__(self):
self.calls = []
def set(self, theme):
self.calls.append(theme)
class _Parent(QWidget):
def __init__(self):
super().__init__()
self.themes = _ThemeSpy()
parent = _Parent()
qtbot.addWidget(parent)
parent = QWidget()
parent.themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
dlg = SettingsDialog(cfg, db, parent=parent)
qtbot.addWidget(dlg)
dlg.show()
qtbot.waitExposed(dlg)
# Change fields
new_path = tmp_path / "new.sqlite"
dlg.path_edit.setText(str(new_path))
dlg.idle_spin.setValue(0)
keys = ["one", "two"]
# User toggles "Remember key" -> stores prompted key
dlg.save_key_btn.setChecked(True)
def _pump_popups():
for w in QApplication.topLevelWidgets():
if isinstance(w, KeyPrompt):
w.edit.setText(keys.pop(0) if keys else "zzz")
w.accept()
elif isinstance(w, QMessageBox):
w.accept()
dlg._save()
out = saved["cfg"]
assert out.path == new_path
assert out.idle_minutes == 0
assert out.key == "sekrit"
assert parent.themes.calls and parent.themes.calls[-1] == Theme.SYSTEM
timer = QTimer()
timer.setInterval(10)
timer.timeout.connect(_pump_popups)
timer.start()
try:
dlg._change_key()
finally:
timer.stop()
db.close()
db2 = DBManager(cfg)
assert db2.connect()
db2.close()
def test_save_key_checkbox_requires_key_and_reverts_if_cancelled(monkeypatch, qtbot):
# When toggled on with no key yet, it prompts; cancelling should revert the check
monkeypatch.setattr("bouquin.settings_dialog.KeyPrompt", RejectingPrompt)
def test_change_key_success(qtbot, tmp_path, app):
from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QApplication, QWidget, QMessageBox
from bouquin.key_prompt import KeyPrompt
from bouquin.db import DBManager, DBConfig
from bouquin.theme import ThemeManager, ThemeConfig, Theme
dlg = SettingsDialog(DBConfig(Path("x"), key=""), FakeDB())
qtbot.addWidget(dlg)
dlg.show()
qtbot.waitExposed(dlg)
assert dlg.key == ""
dlg.save_key_btn.click() # toggles True -> triggers prompt which rejects
assert dlg.save_key_btn.isChecked() is False
assert dlg.key == ""
def test_save_key_checkbox_accepts_and_stores_key(monkeypatch, qtbot):
# Toggling on with an accepting prompt should store the typed key
p = AcceptingPrompt().set_key("remember-me")
monkeypatch.setattr("bouquin.settings_dialog.KeyPrompt", lambda *a, **k: p)
dlg = SettingsDialog(DBConfig(Path("x"), key=""), FakeDB())
qtbot.addWidget(dlg)
dlg.show()
qtbot.waitExposed(dlg)
dlg.save_key_btn.click()
assert dlg.save_key_btn.isChecked() is True
assert dlg.key == "remember-me"
def test_change_key_success(monkeypatch, qtbot):
# Two prompts returning the same non-empty key -> rekey() and info message
p1 = AcceptingPrompt().set_key("newkey")
p2 = AcceptingPrompt().set_key("newkey")
seq = [p1, p2]
monkeypatch.setattr("bouquin.settings_dialog.KeyPrompt", lambda *a, **k: seq.pop(0))
shown = {"info": 0}
monkeypatch.setattr(
QMessageBox,
"information",
lambda *a, **k: shown.__setitem__("info", shown["info"] + 1),
cfg = DBConfig(
path=tmp_path / "iso2.db",
key="oldkey",
idle_minutes=0,
theme="light",
move_todos=True,
)
db = DBManager(cfg)
assert db.connect()
db.save_new_version("2001-01-01", "seed", "seed")
db = FakeDB()
dlg = SettingsDialog(DBConfig(Path("x"), key=""), db)
parent = QWidget()
parent.themes = ThemeManager(app, ThemeConfig(theme=Theme.LIGHT))
dlg = SettingsDialog(cfg, db, parent=parent)
qtbot.addWidget(dlg)
dlg.show()
qtbot.waitExposed(dlg)
dlg._change_key()
keys = ["newkey", "newkey"]
assert db.rekey_called_with == "newkey"
assert shown["info"] >= 1
assert dlg.key == "newkey"
def _pump():
for w in QApplication.topLevelWidgets():
if isinstance(w, KeyPrompt):
w.edit.setText(keys.pop(0) if keys else "newkey")
w.accept()
elif isinstance(w, QMessageBox):
w.accept()
timer = QTimer()
timer.setInterval(10)
timer.timeout.connect(_pump)
timer.start()
try:
dlg._change_key()
finally:
timer.stop()
qtbot.wait(50)
def test_change_key_mismatch_shows_warning_and_no_rekey(monkeypatch, qtbot):
p1 = AcceptingPrompt().set_key("a")
p2 = AcceptingPrompt().set_key("b")
seq = [p1, p2]
monkeypatch.setattr("bouquin.settings_dialog.KeyPrompt", lambda *a, **k: seq.pop(0))
called = {"warn": 0}
monkeypatch.setattr(
QMessageBox,
"warning",
lambda *a, **k: called.__setitem__("warn", called["warn"] + 1),
)
db = FakeDB()
dlg = SettingsDialog(DBConfig(Path("x"), key=""), db)
qtbot.addWidget(dlg)
dlg.show()
qtbot.waitExposed(dlg)
dlg._change_key()
assert db.rekey_called_with is None
assert called["warn"] >= 1
def test_change_key_empty_shows_warning(monkeypatch, qtbot):
p1 = AcceptingPrompt().set_key("")
p2 = AcceptingPrompt().set_key("")
seq = [p1, p2]
monkeypatch.setattr("bouquin.settings_dialog.KeyPrompt", lambda *a, **k: seq.pop(0))
called = {"warn": 0}
monkeypatch.setattr(
QMessageBox,
"warning",
lambda *a, **k: called.__setitem__("warn", called["warn"] + 1),
)
db = FakeDB()
dlg = SettingsDialog(DBConfig(Path("x"), key=""), db)
qtbot.addWidget(dlg)
dlg.show()
qtbot.waitExposed(dlg)
dlg._change_key()
assert db.rekey_called_with is None
assert called["warn"] >= 1
def test_browse_sets_path(monkeypatch, qtbot, tmp_path):
def fake_get_save_file_name(*a, **k):
return (str(tmp_path / "picked.sqlite"), "")
monkeypatch.setattr(
QFileDialog, "getSaveFileName", staticmethod(fake_get_save_file_name)
)
dlg = SettingsDialog(DBConfig(Path("x"), key=""), FakeDB())
qtbot.addWidget(dlg)
dlg.show()
qtbot.waitExposed(dlg)
dlg._browse()
assert dlg.path_edit.text().endswith("picked.sqlite")
def test_compact_success_and_failure(monkeypatch, qtbot):
shown = {"info": 0, "crit": 0}
monkeypatch.setattr(
QMessageBox,
"information",
lambda *a, **k: shown.__setitem__("info", shown["info"] + 1),
)
monkeypatch.setattr(
QMessageBox,
"critical",
lambda *a, **k: shown.__setitem__("crit", shown["crit"] + 1),
)
db = FakeDB()
dlg = SettingsDialog(DBConfig(Path("x"), key=""), db)
qtbot.addWidget(dlg)
dlg.show()
qtbot.waitExposed(dlg)
dlg._compact_btn_clicked()
assert db.compact_called is True
assert shown["info"] >= 1
# Failure path
db2 = FakeDB()
db2.fail_compact = True
dlg2 = SettingsDialog(DBConfig(Path("x"), key=""), db2)
qtbot.addWidget(dlg2)
dlg2.show()
qtbot.waitExposed(dlg2)
dlg2._compact_btn_clicked()
assert shown["crit"] >= 1
def test_save_key_checkbox_preexisting_key_does_not_crash(monkeypatch, qtbot):
p = AcceptingPrompt().set_key("already")
monkeypatch.setattr("bouquin.settings_dialog.KeyPrompt", lambda *a, **k: p)
dlg = SettingsDialog(DBConfig(Path("x"), key="already"), FakeDB())
qtbot.addWidget(dlg)
dlg.show()
qtbot.waitExposed(dlg)
dlg.save_key_btn.setChecked(True)
# We should reach here with the original key preserved.
assert dlg.key == "already"
def test_save_unchecked_clears_key_and_applies_theme(qtbot, tmp_path):
parent = _Parent()
qtbot.addWidget(parent)
cfg = DBConfig(tmp_path / "db.sqlite", key="sekrit", idle_minutes=5)
dlg = SettingsDialog(cfg, FakeDB(), parent=parent)
qtbot.addWidget(dlg)
dlg.save_key_btn.setChecked(False)
# Trigger save
dlg._save()
assert dlg.config.key == "" # cleared
assert parent.themes.calls # applied some theme
db.close()
cfg.key = "newkey"
db2 = DBManager(cfg)
assert db2.connect()
assert "seed" in db2.get_entry("2001-01-01")
db2.close()