aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/test-wttrin--make-cache-key.el19
-rw-r--r--tests/test-wttrin-favorite-override.el13
-rw-r--r--tests/test-wttrin-hide-follow-line.el100
-rw-r--r--tests/test-wttrin-make-default.el9
-rw-r--r--tests/test-wttrin-saved-locations.el9
-rw-r--r--tests/test-wttrin-state-file.el376
-rw-r--r--tests/testutil-wttrin.el19
7 files changed, 530 insertions, 15 deletions
diff --git a/tests/test-wttrin--make-cache-key.el b/tests/test-wttrin--make-cache-key.el
index 79c78d9..f1aca85 100644
--- a/tests/test-wttrin--make-cache-key.el
+++ b/tests/test-wttrin--make-cache-key.el
@@ -62,6 +62,25 @@ Without this, changing the forecast format keeps serving the old cached output."
(should-not (string= a c))
(should-not (string= b c))))
+;; wttrin-hide-follow-line changes the request (it adds the F flag), so it
+;; has to change the key too. Keying on the raw options string missed it and
+;; served a cached buffer that still showed the Follow line.
+(ert-deftest test-wttrin--make-cache-key-boundary-hide-follow-line-changes-key ()
+ "Boundary: turning `wttrin-hide-follow-line' on or off yields a distinct key."
+ (let ((off (let ((wttrin-display-options nil) (wttrin-hide-follow-line nil))
+ (wttrin--make-cache-key "Paris")))
+ (on (let ((wttrin-display-options nil) (wttrin-hide-follow-line t))
+ (wttrin--make-cache-key "Paris"))))
+ (should-not (string= off on))))
+
+(ert-deftest test-wttrin--make-cache-key-boundary-equivalent-requests-share-key ()
+ "Boundary: the toggle and an explicit F flag send one request, so one key."
+ (let ((toggle (let ((wttrin-display-options nil) (wttrin-hide-follow-line t))
+ (wttrin--make-cache-key "Paris")))
+ (flag (let ((wttrin-display-options "F") (wttrin-hide-follow-line nil))
+ (wttrin--make-cache-key "Paris"))))
+ (should (string= toggle flag))))
+
(ert-deftest test-wttrin--make-cache-key-boundary-language-changes-key ()
"Boundary: changing Accept-Language yields a distinct key."
(let ((en (let ((wttrin-default-languages '("Accept-Language" . "en-US")))
diff --git a/tests/test-wttrin-favorite-override.el b/tests/test-wttrin-favorite-override.el
index 0236901..95916c7 100644
--- a/tests/test-wttrin-favorite-override.el
+++ b/tests/test-wttrin-favorite-override.el
@@ -64,15 +64,18 @@ with `d' reverted to the init value on restart."
(should (equal "New Orleans, LA" wttrin-favorite-location))
(should (equal "Paris, FR" (wttrin--favorite-location)))))
-;;; savehist registration targets the runtime vars, not the config defcustoms
+;;; savehist registration covers only the scrub-tolerant search history;
+;;; the runtime vars persist in `wttrin-state-file' (see test-wttrin-state-file.el)
-(ert-deftest test-wttrin-favorite-override-normal-savehist-registers-override ()
- "Normal: savehist persists the override, not the config defcustom."
+(ert-deftest test-wttrin-favorite-override-normal-savehist-excludes-runtime-vars ()
+ "Normal: savehist persists neither the runtime vars nor the config defcustoms.
+The runtime favorite and directory live in `wttrin-state-file' — a savehist
+entry would be scrubbed by any session that saves savehist without wttrin."
(require 'savehist)
(let ((savehist-additional-variables '(kill-ring)))
(wttrin--savehist-register)
- (should (memq 'wttrin--favorite-override savehist-additional-variables))
- (should (memq 'wttrin--saved-locations-runtime savehist-additional-variables))
+ (should-not (memq 'wttrin--favorite-override savehist-additional-variables))
+ (should-not (memq 'wttrin--saved-locations-runtime savehist-additional-variables))
(should-not (memq 'wttrin-favorite-location savehist-additional-variables))
(should-not (memq 'wttrin-saved-locations savehist-additional-variables))))
diff --git a/tests/test-wttrin-hide-follow-line.el b/tests/test-wttrin-hide-follow-line.el
new file mode 100644
index 0000000..742a872
--- /dev/null
+++ b/tests/test-wttrin-hide-follow-line.el
@@ -0,0 +1,100 @@
+;;; test-wttrin-hide-follow-line.el --- Tests for hiding the follow line -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2026 Craig Jennings
+
+;;; Commentary:
+
+;; Unit tests for wttrin--effective-display-options and its effect on
+;; wttrin--build-url: the wttr.in F flag is added to the buffer request when
+;; wttrin-hide-follow-line is non-nil, composing with wttrin-display-options.
+
+;;; Code:
+
+(require 'ert)
+(require 'wttrin)
+
+;;; --------------------------------------------------------------------------
+;;; wttrin--effective-display-options
+;;; --------------------------------------------------------------------------
+
+;;; Normal Cases
+
+(ert-deftest test-wttrin--effective-display-options-normal-hide-empty-adds-f ()
+ "Normal: hiding with no other options yields just the F flag."
+ (let ((wttrin-display-options nil)
+ (wttrin-hide-follow-line t))
+ (should (equal (wttrin--effective-display-options) "F"))))
+
+(ert-deftest test-wttrin--effective-display-options-normal-hide-appends-f ()
+ "Normal: hiding appends F to existing options."
+ (let ((wttrin-display-options "0q")
+ (wttrin-hide-follow-line t))
+ (should (equal (wttrin--effective-display-options) "0qF"))))
+
+(ert-deftest test-wttrin--effective-display-options-normal-no-hide-unchanged ()
+ "Normal: not hiding leaves the options untouched."
+ (let ((wttrin-display-options "0q")
+ (wttrin-hide-follow-line nil))
+ (should (equal (wttrin--effective-display-options) "0q"))))
+
+;;; Boundary Cases
+
+(ert-deftest test-wttrin--effective-display-options-boundary-f-already-present ()
+ "Boundary: F already in the options is not duplicated."
+ (let ((wttrin-display-options "0Fq")
+ (wttrin-hide-follow-line t))
+ (should (equal (wttrin--effective-display-options) "0Fq"))))
+
+(ert-deftest test-wttrin--effective-display-options-boundary-no-hide-nil-options ()
+ "Boundary: not hiding with nil options yields an empty string."
+ (let ((wttrin-display-options nil)
+ (wttrin-hide-follow-line nil))
+ (should (equal (wttrin--effective-display-options) ""))))
+
+;; wttr.in flags are case-sensitive (q and Q differ), and string-match-p
+;; honours `case-fold-search', which defaults to t. A lowercase f must not
+;; read as the F flag, whatever the caller's case-fold setting is.
+(ert-deftest test-wttrin--effective-display-options-boundary-lowercase-f-is-not-f ()
+ "Boundary: a lowercase f in the options does not count as the F flag."
+ (let ((wttrin-display-options "0f")
+ (wttrin-hide-follow-line t)
+ (case-fold-search t))
+ (should (equal (wttrin--effective-display-options) "0fF"))))
+
+(ert-deftest test-wttrin--effective-display-options-boundary-case-fold-nil-same-result ()
+ "Boundary: the result does not depend on the caller's `case-fold-search'."
+ (let ((wttrin-display-options "0f")
+ (wttrin-hide-follow-line t)
+ (case-fold-search nil))
+ (should (equal (wttrin--effective-display-options) "0fF"))))
+
+;;; Error Cases
+
+(ert-deftest test-wttrin--effective-display-options-error-non-string-options ()
+ "Error: a non-string options value signals rather than building a bad URL."
+ (let ((wttrin-display-options 'bogus)
+ (wttrin-hide-follow-line t))
+ (should-error (wttrin--effective-display-options) :type 'wrong-type-argument)))
+
+;;; --------------------------------------------------------------------------
+;;; wttrin--build-url integration
+;;; --------------------------------------------------------------------------
+
+;;; Normal Cases
+
+(ert-deftest test-wttrin--build-url-normal-hide-appends-f-to-url ()
+ "Normal: hiding the follow line puts the F flag at the end of the URL."
+ (let ((wttrin-display-options nil)
+ (wttrin-hide-follow-line t)
+ (wttrin-unit-system nil))
+ (should (string-suffix-p "F" (wttrin--build-url "Tokyo")))))
+
+(ert-deftest test-wttrin--build-url-normal-no-hide-no-trailing-f ()
+ "Normal: with hiding off, the URL does not end in the F flag."
+ (let ((wttrin-display-options nil)
+ (wttrin-hide-follow-line nil)
+ (wttrin-unit-system nil))
+ (should-not (string-suffix-p "F" (wttrin--build-url "Tokyo")))))
+
+(provide 'test-wttrin-hide-follow-line)
+;;; test-wttrin-hide-follow-line.el ends here
diff --git a/tests/test-wttrin-make-default.el b/tests/test-wttrin-make-default.el
index e824194..c7954c8 100644
--- a/tests/test-wttrin-make-default.el
+++ b/tests/test-wttrin-make-default.el
@@ -60,13 +60,14 @@ unbound); persistence is left to `wttrin--savehist-register'."
(wttrin--set-favorite-location "Berkeley, CA")
(should (equal wttrin--location-history '("Oslo, NO")))))
-(ert-deftest test-wttrin-favorite-savehist-register-includes-favorite ()
- "Normal: `wttrin--savehist-register' registers the runtime override, not the
-`wttrin-favorite-location' defcustom."
+(ert-deftest test-wttrin-favorite-savehist-register-excludes-favorite ()
+ "Normal: `wttrin--savehist-register' does not register the runtime override.
+The favorite persists in `wttrin-state-file' instead — a savehist entry would
+be scrubbed by any Emacs session that saves savehist without wttrin loaded."
(require 'savehist)
(let ((savehist-additional-variables '(kill-ring)))
(wttrin--savehist-register)
- (should (memq 'wttrin--favorite-override savehist-additional-variables))
+ (should-not (memq 'wttrin--favorite-override savehist-additional-variables))
(should-not (memq 'wttrin-favorite-location savehist-additional-variables))))
;;; --------------------------------------------------------------------------
diff --git a/tests/test-wttrin-saved-locations.el b/tests/test-wttrin-saved-locations.el
index 1a08aaf..b41d7ee 100644
--- a/tests/test-wttrin-saved-locations.el
+++ b/tests/test-wttrin-saved-locations.el
@@ -159,13 +159,14 @@ Two names with the same query share a key; the name never leaks into the key."
;;; savehist
-(ert-deftest test-wttrin-saved-locations-integration-savehist-registers ()
- "Integration: the runtime directory is registered for savehist persistence,
-not the `wttrin-saved-locations' defcustom (which the user sets in init)."
+(ert-deftest test-wttrin-saved-locations-integration-savehist-excludes-runtime ()
+ "Integration: the runtime directory is not registered with savehist.
+It persists in `wttrin-state-file' instead — a savehist entry would be
+scrubbed by any Emacs session that saves savehist without wttrin loaded."
(require 'savehist)
(let ((savehist-additional-variables '(kill-ring)))
(wttrin--savehist-register)
- (should (memq 'wttrin--saved-locations-runtime savehist-additional-variables))
+ (should-not (memq 'wttrin--saved-locations-runtime savehist-additional-variables))
(should-not (memq 'wttrin-saved-locations savehist-additional-variables))))
;;; wttrin--coordinates-p
diff --git a/tests/test-wttrin-state-file.el b/tests/test-wttrin-state-file.el
new file mode 100644
index 0000000..34f8eb3
--- /dev/null
+++ b/tests/test-wttrin-state-file.el
@@ -0,0 +1,376 @@
+;;; test-wttrin-state-file.el --- Tests for state-file persistence -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2026 Craig Jennings
+
+;;; Commentary:
+
+;; Unit tests for the wttrin state file: `wttrin--state-save',
+;; `wttrin--state-read', and `wttrin--state-load'. The state file persists
+;; the runtime favorite (`wttrin--favorite-override') and runtime directory
+;; (`wttrin--saved-locations-runtime') in a file only wttrin writes, so a
+;; foreign Emacs process saving savehist without wttrin loaded can no longer
+;; scrub them. Covers round-trips, authority over in-memory values, the
+;; savehist-legacy adoption path, setter integration, and failure isolation.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+(require 'wttrin)
+
+(defmacro test-wttrin-state-file--with-sandbox (&rest body)
+ "Run BODY with `wttrin-state-file' bound to a fresh temp path.
+The runtime vars start nil. The file is removed afterward."
+ (declare (indent 0))
+ `(let ((wttrin-state-file (expand-file-name
+ (format "wttrin-test-state-%s.el" (random 1000000))
+ temporary-file-directory))
+ (wttrin--favorite-override nil)
+ (wttrin--saved-locations-runtime nil))
+ (unwind-protect
+ (progn ,@body)
+ (when (file-exists-p wttrin-state-file)
+ (delete-file wttrin-state-file)))))
+
+;;; --------------------------------------------------------------------------
+;;; Normal Cases
+;;; --------------------------------------------------------------------------
+
+(ert-deftest test-wttrin-state-file-normal-save-load-round-trip ()
+ "Normal: save writes both vars; load restores them after they are cleared."
+ (test-wttrin-state-file--with-sandbox
+ (setq wttrin--favorite-override "Hyatt Place Warwick, RI")
+ (setq wttrin--saved-locations-runtime
+ '(("Hyatt Place Warwick, RI" . "41.7266678,-71.443097")))
+ (wttrin--state-save)
+ (setq wttrin--favorite-override nil)
+ (setq wttrin--saved-locations-runtime nil)
+ (wttrin--state-load)
+ (should (equal wttrin--favorite-override "Hyatt Place Warwick, RI"))
+ (should (equal wttrin--saved-locations-runtime
+ '(("Hyatt Place Warwick, RI" . "41.7266678,-71.443097"))))))
+
+(ert-deftest test-wttrin-state-file-normal-file-wins-over-memory ()
+ "Normal: an existing state file is authoritative over in-memory values.
+This is the guard against a later savehist restore of stale legacy lines."
+ (test-wttrin-state-file--with-sandbox
+ (setq wttrin--favorite-override "Newer, ME")
+ (wttrin--state-save)
+ (setq wttrin--favorite-override "Stale Legacy, LA")
+ (setq wttrin--saved-locations-runtime '(("Stale" . "stale")))
+ (wttrin--state-load)
+ (should (equal wttrin--favorite-override "Newer, ME"))
+ (should (null wttrin--saved-locations-runtime))))
+
+(ert-deftest test-wttrin-state-file-normal-set-favorite-writes-file ()
+ "Normal: `wttrin--set-favorite-location' persists to the state file."
+ (test-wttrin-state-file--with-sandbox
+ (let ((wttrin-favorite-location nil)
+ (wttrin--location-history nil))
+ (wttrin--set-favorite-location "Paris, FR")
+ (should (file-exists-p wttrin-state-file))
+ (should (equal (plist-get (wttrin--state-read) :favorite-override)
+ "Paris, FR")))))
+
+(ert-deftest test-wttrin-state-file-normal-put-saved-location-writes-file ()
+ "Normal: `wttrin--put-saved-location' persists to the state file."
+ (test-wttrin-state-file--with-sandbox
+ (wttrin--put-saved-location "Home" "New Orleans, LA")
+ (should (equal (plist-get (wttrin--state-read) :saved-locations)
+ '(("Home" . "New Orleans, LA"))))))
+
+(ert-deftest test-wttrin-state-file-normal-remove-saved-location-writes-file ()
+ "Normal: `wttrin--remove-saved-location' persists the removal."
+ (test-wttrin-state-file--with-sandbox
+ (wttrin--put-saved-location "Home" "New Orleans, LA")
+ (wttrin--remove-saved-location "Home")
+ (should (null (plist-get (wttrin--state-read) :saved-locations)))))
+
+(ert-deftest test-wttrin-state-file-normal-rename-location-writes-file ()
+ "Normal: `wttrin-rename-location' persists the rename and favorite update."
+ (test-wttrin-state-file--with-sandbox
+ (let ((wttrin-saved-locations nil)
+ (wttrin-favorite-location nil)
+ (wttrin--location-history nil))
+ (wttrin--put-saved-location "Hotel" "41.72,-71.44")
+ (wttrin--set-favorite-location "Hotel")
+ (wttrin-rename-location "Hotel" "Hyatt")
+ (let ((data (wttrin--state-read)))
+ (should (equal (plist-get data :saved-locations)
+ '(("Hyatt" . "41.72,-71.44"))))
+ (should (equal (plist-get data :favorite-override) "Hyatt"))))))
+
+(ert-deftest test-wttrin-state-file-normal-adopts-savehist-legacy-values ()
+ "Normal: with no state file, non-nil vars (savehist legacy) are adopted."
+ (test-wttrin-state-file--with-sandbox
+ (setq wttrin--favorite-override "Legacy, TX")
+ (wttrin--state-load)
+ (should (file-exists-p wttrin-state-file))
+ (should (equal (plist-get (wttrin--state-read) :favorite-override)
+ "Legacy, TX"))))
+
+(ert-deftest test-wttrin-state-file-normal-savehist-mode-hook-wired ()
+ "Normal: `wttrin--state-load' is on `savehist-mode-hook' so a savehist
+restore after wttrin loads cannot clobber state-file values."
+ (should (memq #'wttrin--state-load savehist-mode-hook)))
+
+;;; --------------------------------------------------------------------------
+;;; Boundary Cases
+;;; --------------------------------------------------------------------------
+
+(ert-deftest test-wttrin-state-file-boundary-tri-state-t-round-trips ()
+ "Boundary: the favorite's auto-detect value t survives a round-trip."
+ (test-wttrin-state-file--with-sandbox
+ (setq wttrin--favorite-override t)
+ (wttrin--state-save)
+ (setq wttrin--favorite-override nil)
+ (wttrin--state-load)
+ (should (eq wttrin--favorite-override t))))
+
+(ert-deftest test-wttrin-state-file-boundary-nils-round-trip ()
+ "Boundary: an explicitly saved all-nil state loads as nils."
+ (test-wttrin-state-file--with-sandbox
+ (wttrin--state-save)
+ (setq wttrin--favorite-override "Ghost, AZ")
+ (setq wttrin--saved-locations-runtime '(("Ghost" . "ghost")))
+ (wttrin--state-load)
+ (should (null wttrin--favorite-override))
+ (should (null wttrin--saved-locations-runtime))))
+
+(ert-deftest test-wttrin-state-file-boundary-long-directory-round-trips ()
+ "Boundary: a long saved-locations alist survives intact.
+Guards the `print-length' / `print-level' bindings in the writer."
+ (test-wttrin-state-file--with-sandbox
+ (let ((entries (cl-loop for i from 1 to 60
+ collect (cons (format "Place %02d" i)
+ (format "%d.0,-%d.0" i i)))))
+ (setq wttrin--saved-locations-runtime entries)
+ (wttrin--state-save)
+ (setq wttrin--saved-locations-runtime nil)
+ (wttrin--state-load)
+ (should (equal wttrin--saved-locations-runtime entries))
+ (should (= (length wttrin--saved-locations-runtime) 60)))))
+
+(ert-deftest test-wttrin-state-file-boundary-unicode-round-trips ()
+ "Boundary: unicode names and queries survive a round-trip."
+ (test-wttrin-state-file--with-sandbox
+ (setq wttrin--favorite-override "Zürich 🌦️")
+ (setq wttrin--saved-locations-runtime '(("北京" . "Beijing, CN")))
+ (wttrin--state-save)
+ (setq wttrin--favorite-override nil)
+ (setq wttrin--saved-locations-runtime nil)
+ (wttrin--state-load)
+ (should (equal wttrin--favorite-override "Zürich 🌦️"))
+ (should (equal wttrin--saved-locations-runtime '(("北京" . "Beijing, CN"))))))
+
+(ert-deftest test-wttrin-state-file-boundary-read-absent-file-returns-nil ()
+ "Boundary: reading a nonexistent state file returns nil."
+ (test-wttrin-state-file--with-sandbox
+ (should-not (wttrin--state-read))))
+
+(ert-deftest test-wttrin-state-file-boundary-absent-file-nil-vars-noop ()
+ "Boundary: no file and nil vars is a silent no-op — no file created."
+ (test-wttrin-state-file--with-sandbox
+ (wttrin--state-load)
+ (should-not (file-exists-p wttrin-state-file))
+ (should (null wttrin--favorite-override))
+ (should (null wttrin--saved-locations-runtime))))
+
+;;; --------------------------------------------------------------------------
+;;; Error Cases
+;;; --------------------------------------------------------------------------
+
+(ert-deftest test-wttrin-state-file-error-corrupt-file-does-not-signal ()
+ "Error: unreadable lisp in the state file is ignored, vars untouched."
+ (test-wttrin-state-file--with-sandbox
+ (with-temp-file wttrin-state-file (insert "(((( not lisp"))
+ (setq wttrin--favorite-override "Kept, OK")
+ (wttrin--state-load)
+ (should (equal wttrin--favorite-override "Kept, OK"))))
+
+(ert-deftest test-wttrin-state-file-error-empty-file-does-not-signal ()
+ "Error: an empty state file is ignored, vars untouched."
+ (test-wttrin-state-file--with-sandbox
+ (with-temp-file wttrin-state-file)
+ (setq wttrin--favorite-override "Kept, OK")
+ (wttrin--state-load)
+ (should (equal wttrin--favorite-override "Kept, OK"))))
+
+(ert-deftest test-wttrin-state-file-error-wrong-shape-data-ignored ()
+ "Error: readable lisp that is not a versioned plist is treated as corrupt."
+ (test-wttrin-state-file--with-sandbox
+ (with-temp-file wttrin-state-file (insert "[1 2 3]"))
+ (setq wttrin--favorite-override "Kept, OK")
+ (wttrin--state-load)
+ (should (equal wttrin--favorite-override "Kept, OK"))))
+
+(ert-deftest test-wttrin-state-file-error-failed-write-preserves-file ()
+ "Error: a failed write leaves the previous state file intact.
+The writer goes through a temp file + rename, so an error before the
+rename cannot truncate or clobber the existing file."
+ (test-wttrin-state-file--with-sandbox
+ (setq wttrin--favorite-override "Good, OK")
+ (wttrin--state-save)
+ (setq wttrin--favorite-override "Never Written, NV")
+ (cl-letf (((symbol-function 'write-region)
+ (lambda (&rest _) (error "Disk full"))))
+ (wttrin--state-save))
+ (should (equal (plist-get (wttrin--state-read) :favorite-override)
+ "Good, OK"))))
+
+(ert-deftest test-wttrin-state-file-error-corrupt-file-survives-adoption ()
+ "Error: adoption never writes over an existing corrupt state file.
+The file is left byte-for-byte in place for inspection; non-nil vars do
+not trigger the missing-file adoption write because the file exists."
+ (test-wttrin-state-file--with-sandbox
+ (with-temp-file wttrin-state-file (insert "(((( not lisp"))
+ (setq wttrin--favorite-override "Legacy, TX")
+ (wttrin--state-load)
+ (should (equal (with-temp-buffer
+ (insert-file-contents wttrin-state-file)
+ (buffer-string))
+ "(((( not lisp"))))
+
+(ert-deftest test-wttrin-state-file-error-unwritable-path-does-not-signal ()
+ "Error: a save to an unwritable path messages instead of signaling."
+ (let ((wttrin-state-file "/nonexistent-root-dir/wttrin/state.el")
+ (wttrin--favorite-override "Anywhere, US")
+ (wttrin--saved-locations-runtime nil))
+ (should-not
+ (condition-case nil (progn (wttrin--state-save) nil) (error t)))))
+
+;;; --------------------------------------------------------------------------
+;;; savehist registration (post state-file)
+;;; --------------------------------------------------------------------------
+
+(ert-deftest test-wttrin-state-file-normal-savehist-excludes-state-vars ()
+ "Normal: `wttrin--savehist-register' no longer registers the state-file
+vars; only the scrub-tolerant search history stays with savehist."
+ (require 'savehist)
+ (let ((savehist-additional-variables '(kill-ring)))
+ (wttrin--savehist-register)
+ (should (memq 'wttrin--location-history savehist-additional-variables))
+ (should-not (memq 'wttrin--favorite-override savehist-additional-variables))
+ (should-not (memq 'wttrin--saved-locations-runtime
+ savehist-additional-variables))))
+
+;;; --------------------------------------------------------------------------
+;;; Path customization after load (`:set' on `wttrin-state-file')
+;;; --------------------------------------------------------------------------
+
+(defmacro test-wttrin-state-file--with-path-sandbox (&rest body)
+ "Run BODY with a fresh temp PATH bound and the state vars isolated.
+`wttrin-state-file' is let-bound to itself so a `customize-set-variable'
+inside BODY is undone on exit; the `customized-value' property and the temp
+file are cleaned up afterward."
+ (declare (indent 0))
+ `(let ((path (expand-file-name
+ (format "wttrin-test-state-%s.el" (random 1000000))
+ temporary-file-directory))
+ (wttrin-state-file wttrin-state-file)
+ (wttrin--favorite-override nil)
+ (wttrin--saved-locations-runtime nil))
+ (unwind-protect
+ (progn ,@body)
+ (put 'wttrin-state-file 'customized-value nil)
+ (when (file-exists-p path)
+ (delete-file path)))))
+
+(ert-deftest test-wttrin-state-file-normal-set-property-wired ()
+ "Normal: `wttrin-state-file' carries the `:set' handler, so `setopt',
+`customize-set-variable', and Customize all route through it."
+ (should (eq (get 'wttrin-state-file 'custom-set) #'wttrin--state-file-set)))
+
+(ert-deftest test-wttrin-state-file-normal-set-after-load-restores-from-new-path ()
+ "Normal: customizing the path after load restores state from the new file.
+This is the use-package `:config' shape — `(setopt wttrin-state-file ...)'
+after `(require 'wttrin)' — where the load-time restore has already read the
+default path and would otherwise never see the file the setter writes."
+ (test-wttrin-state-file--with-path-sandbox
+ (let ((wttrin-state-file path)
+ (wttrin--favorite-override "Salt Lake City, Utah")
+ (wttrin--saved-locations-runtime '(("SLC" . "40.65,-111.87"))))
+ (wttrin--state-save))
+ (customize-set-variable 'wttrin-state-file path)
+ (should (equal wttrin-state-file path))
+ (should (equal wttrin--favorite-override "Salt Lake City, Utah"))
+ (should (equal wttrin--saved-locations-runtime '(("SLC" . "40.65,-111.87"))))))
+
+(ert-deftest test-wttrin-state-file-normal-set-refreshes-mode-line-on-change ()
+ "Normal: when the restore changes the effective favorite and the mode-line
+mode is on, the mode-line is refreshed so it shows the restored city."
+ (test-wttrin-state-file--with-path-sandbox
+ (let ((wttrin-state-file path)
+ (wttrin--favorite-override "Restored, UT"))
+ (wttrin--state-save))
+ (let ((refreshes 0)
+ (wttrin-favorite-location "Configured, LA")
+ (wttrin-mode-line-mode t))
+ (cl-letf (((symbol-function 'wttrin--mode-line-refresh-now)
+ (lambda () (setq refreshes (1+ refreshes)))))
+ (customize-set-variable 'wttrin-state-file path))
+ (should (= refreshes 1)))))
+
+(ert-deftest test-wttrin-state-file-boundary-set-same-favorite-no-refresh ()
+ "Boundary: a restore that leaves the effective favorite unchanged does not
+refresh the mode-line."
+ (test-wttrin-state-file--with-path-sandbox
+ (let ((wttrin-state-file path)
+ (wttrin--favorite-override "Same, UT"))
+ (wttrin--state-save))
+ (let ((refreshes 0)
+ (wttrin--favorite-override "Same, UT")
+ (wttrin-mode-line-mode t))
+ (cl-letf (((symbol-function 'wttrin--mode-line-refresh-now)
+ (lambda () (setq refreshes (1+ refreshes)))))
+ (customize-set-variable 'wttrin-state-file path))
+ (should (= refreshes 0)))))
+
+(ert-deftest test-wttrin-state-file-boundary-set-mode-line-off-no-refresh ()
+ "Boundary: a favorite change with the mode-line mode off does not refresh."
+ (test-wttrin-state-file--with-path-sandbox
+ (let ((wttrin-state-file path)
+ (wttrin--favorite-override "Restored, UT"))
+ (wttrin--state-save))
+ (let ((refreshes 0)
+ (wttrin-mode-line-mode nil))
+ (cl-letf (((symbol-function 'wttrin--mode-line-refresh-now)
+ (lambda () (setq refreshes (1+ refreshes)))))
+ (customize-set-variable 'wttrin-state-file path))
+ (should (equal wttrin--favorite-override "Restored, UT"))
+ (should (= refreshes 0)))))
+
+(ert-deftest test-wttrin-state-file-boundary-set-absent-file-nil-vars-noop ()
+ "Boundary: customizing to a path with no file and nil vars only sets the
+path — nothing is restored and no file is created."
+ (test-wttrin-state-file--with-path-sandbox
+ (customize-set-variable 'wttrin-state-file path)
+ (should (equal wttrin-state-file path))
+ (should-not (file-exists-p path))
+ (should (null wttrin--favorite-override))))
+
+(ert-deftest test-wttrin-state-file-boundary-set-absent-file-adopts-vars ()
+ "Boundary: customizing to a path with no file while the vars hold values
+adopts them into the new file, the same contract as `wttrin--state-load'."
+ (test-wttrin-state-file--with-path-sandbox
+ (setq wttrin--favorite-override "Adopted, TX")
+ (customize-set-variable 'wttrin-state-file path)
+ (should (file-exists-p path))
+ (should (equal (plist-get (wttrin--state-read) :favorite-override)
+ "Adopted, TX"))))
+
+(ert-deftest test-wttrin-state-file-error-set-corrupt-file-does-not-signal ()
+ "Error: customizing to a path holding a corrupt file sets the path, leaves
+the vars untouched, and does not signal."
+ (test-wttrin-state-file--with-path-sandbox
+ (with-temp-file path (insert "(((( not lisp"))
+ (setq wttrin--favorite-override "Kept, OK")
+ (should-not
+ (condition-case nil
+ (progn (customize-set-variable 'wttrin-state-file path) nil)
+ (error t)))
+ (should (equal wttrin-state-file path))
+ (should (equal wttrin--favorite-override "Kept, OK"))))
+
+(provide 'test-wttrin-state-file)
+;;; test-wttrin-state-file.el ends here
diff --git a/tests/testutil-wttrin.el b/tests/testutil-wttrin.el
index e4e2e4e..1373813 100644
--- a/tests/testutil-wttrin.el
+++ b/tests/testutil-wttrin.el
@@ -136,17 +136,32 @@ Other mocks BODY needs can be set in a nested `cl-letf'."
;;; Test Setup and Teardown
+;; Redirect state-file writes for the whole batch: any test that exercises a
+;; location setter triggers `wttrin--state-save', which must never touch the
+;; developer's real `wttrin-state-file'. Process-wide, set once at load.
+(setq wttrin-state-file
+ (expand-file-name (format "wttrin-test-state-%d.el" (emacs-pid))
+ temporary-file-directory))
+
(defun testutil-wttrin-setup ()
"Common setup for wttrin tests.
Call this at the beginning of each test."
(testutil-wttrin-clear-cache)
- (setq wttrin--force-refresh nil))
+ (setq wttrin--force-refresh nil)
+ (setq wttrin--favorite-override nil)
+ (setq wttrin--saved-locations-runtime nil)
+ (when (file-exists-p wttrin-state-file)
+ (delete-file wttrin-state-file)))
(defun testutil-wttrin-teardown ()
"Common teardown for wttrin tests.
Call this at the end of each test."
(testutil-wttrin-clear-cache)
- (setq wttrin--force-refresh nil))
+ (setq wttrin--force-refresh nil)
+ (setq wttrin--favorite-override nil)
+ (setq wttrin--saved-locations-runtime nil)
+ (when (file-exists-p wttrin-state-file)
+ (delete-file wttrin-state-file)))
(provide 'testutil-wttrin)
;;; testutil-wttrin.el ends here