aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-14 19:32:02 +0200
committeruser <user@clank>2026-08-14 19:32:02 +0200
commit6a6799bfc0eb8662df12b636e4fe537b84a5d484 (patch)
tree887b96ef94def60fad351cf41070a86be94a7f56 /tests
parentsl0ppty: saving the config reloads it (diff)
downloadslosh-6a6799bfc0eb8662df12b636e4fe537b84a5d484.tar.gz
slosh-6a6799bfc0eb8662df12b636e4fe537b84a5d484.tar.xz
slosh-6a6799bfc0eb8662df12b636e4fe537b84a5d484.zip
sl0ppty: one save, one reload
A double "config reloaded" was reported, and the immediate cause was me saving the file twice in a row — but looking properly turned up a real one underneath. One save is not always one inotify event. Watching a directory for CLOSE_WRITE, MOVED_TO and CREATE, an editor that *creates* the file rather than rewriting it produces CREATE and then CLOSE_WRITE, measured, and each one was reloading the config and announcing it. Rewriting in place gives one event, and so does the rename-over that sed and vim do, which is why it took a specific kind of editor to see it. Events now arm a deadline rather than reloading, and the reload happens once the file has been quiet for eighty milliseconds. That collapses the double, and also means an editor that writes in chunks is read after it has finished rather than halfway through — which should make the mid-save parse error, the thing the fail-open behaviour exists for, rarer than it was. The test counts the toasts, because toasts stack: a config that reloaded twice says so twice, which is exactly what was seen on screen. I checked it against the un-debounced build first and it reported two, so it is testing the thing it claims to.
Diffstat (limited to 'tests')
-rw-r--r--tests/live_reload.py38
1 files changed, 38 insertions, 0 deletions
diff --git a/tests/live_reload.py b/tests/live_reload.py
index 9d1ab2c..db1fff8 100644
--- a/tests/live_reload.py
+++ b/tests/live_reload.py
@@ -120,6 +120,43 @@ def test_watch():
os.unlink(f)
+def toast_count(session, text):
+ """How many times `text` appears on screen. Toasts stack, so a config that
+ reloaded twice announces itself twice."""
+ out = subprocess.run([BIN, "-s", session, "cmd", "snapshot"],
+ capture_output=True, text=True).stdout
+ try:
+ rows = json.loads(out)["text"]
+ except Exception:
+ return -1
+ return sum(1 for r in rows if text in r)
+
+
+def test_one_save_is_one_reload():
+ """An editor that creates the file rather than rewriting it produces two
+ inotify events -- CREATE then CLOSE_WRITE -- and reloading on each would
+ parse the config twice and say so twice for a single edit."""
+ name = "t" + uuid.uuid4().hex[:8]
+ cfg = f"/tmp/{name}.kdl"
+ write(cfg, "#00ff00")
+ p = start(name, cfg)
+ try:
+ # Recreate it the way an editor that unlinks first would.
+ os.unlink(cfg)
+ write(cfg, "#ff0000")
+ check("the recreated file is picked up",
+ wait_for_colour(name, "#ff0000") == "#ff0000",
+ str(frame_colour(name)))
+ time.sleep(0.3) # bounded: let any second reload land if it is coming
+ n = toast_count(name, "config reloaded")
+ check("and announced exactly once, not once per inotify event",
+ n == 1, f"{n} toasts")
+ finally:
+ stop(name, p)
+ if os.path.exists(cfg):
+ os.unlink(cfg)
+
+
def test_no_reload():
name = "t" + uuid.uuid4().hex[:8]
cfg = f"/tmp/{name}.kdl"
@@ -147,6 +184,7 @@ def test_no_reload():
if __name__ == "__main__":
test_watch()
+ test_one_save_is_one_reload()
test_no_reload()
print()
print(f"{'FAILED' if fails else 'all green'} ({fails} failures)")