diff options
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test-wttrin--mode-line-forecast.el | 149 | ||||
| -rw-r--r-- | tests/test-wttrin--render-loading-placeholder.el | 64 | ||||
| -rw-r--r-- | tests/test-wttrin-favorite-override.el | 83 | ||||
| -rw-r--r-- | tests/test-wttrin-make-default.el | 48 | ||||
| -rw-r--r-- | tests/test-wttrin-saved-locations.el | 188 | ||||
| -rw-r--r-- | tests/test-wttrin-set-location-from-geolocation.el | 22 | ||||
| -rw-r--r-- | tests/test-wttrin-state-file.el | 258 | ||||
| -rw-r--r-- | tests/test-wttrin-use-current-location.el | 18 | ||||
| -rw-r--r-- | tests/testutil-wttrin.el | 19 |
9 files changed, 752 insertions, 97 deletions
diff --git a/tests/test-wttrin--mode-line-forecast.el b/tests/test-wttrin--mode-line-forecast.el new file mode 100644 index 0000000..c371f94 --- /dev/null +++ b/tests/test-wttrin--mode-line-forecast.el @@ -0,0 +1,149 @@ +;;; test-wttrin--mode-line-forecast.el --- Tests for the tooltip forecast -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Craig Jennings + +;;; Commentary: + +;; Unit tests for the mode-line tooltip forecast: parsing wttr.in's j1 JSON +;; into a day list, formatting up to `wttrin-mode-line-tooltip-forecast-days' +;; lines, and appending the block to `wttrin--mode-line-tooltip' output. + +;;; Code: + +(require 'ert) +(require 'wttrin) +(require 'testutil-wttrin) + +;;; Fixture + +(defconst test-wttrin--forecast-j1 + "{\"weather\": [ + {\"date\": \"2026-07-01\", \"mintempC\": \"22\", \"maxtempC\": \"31\", + \"mintempF\": \"71\", \"maxtempF\": \"88\", + \"hourly\": [{\"time\": \"0\", \"weatherDesc\": [{\"value\": \"Clear\"}]}, + {\"time\": \"1200\", \"weatherDesc\": [{\"value\": \"Partly cloudy\"}]}, + {\"time\": \"2100\", \"weatherDesc\": [{\"value\": \"Clear\"}]}]}, + {\"date\": \"2026-07-02\", \"mintempC\": \"20\", \"maxtempC\": \"26\", + \"mintempF\": \"68\", \"maxtempF\": \"79\", + \"hourly\": [{\"time\": \"1200\", \"weatherDesc\": [{\"value\": \"Light rain\"}]}]}, + {\"date\": \"2026-07-03\", \"mintempC\": \"21\", \"maxtempC\": \"31\", + \"mintempF\": \"70\", \"maxtempF\": \"88\", + \"hourly\": [{\"time\": \"1200\", \"weatherDesc\": [{\"value\": \"Sunny\"}]}]}]}" + "Trimmed wttr.in ?format=j1 response: three days, the keys the code reads.") + +(defun test-wttrin--forecast-days () + "Return the parsed day list from the fixture." + (wttrin--forecast-parse test-wttrin--forecast-j1)) + +;;; -------------------------------------------------------------------------- +;;; wttrin--forecast-parse +;;; -------------------------------------------------------------------------- + +(ert-deftest test-wttrin--forecast-parse-normal-three-days () + "Normal: the fixture parses into a list of three day alists." + (let ((days (test-wttrin--forecast-days))) + (should (= 3 (length days))) + (should (equal "2026-07-01" (alist-get 'date (car days)))))) + +(ert-deftest test-wttrin--forecast-parse-error-malformed-json () + "Error: malformed JSON returns nil instead of signaling." + (should-not (wttrin--forecast-parse "{not json"))) + +(ert-deftest test-wttrin--forecast-parse-error-missing-weather-key () + "Error: JSON without a weather array returns nil." + (should-not (wttrin--forecast-parse "{\"nearest_area\": []}"))) + +(ert-deftest test-wttrin--forecast-parse-boundary-nil-and-empty () + "Boundary: nil and empty-string input return nil." + (should-not (wttrin--forecast-parse nil)) + (should-not (wttrin--forecast-parse ""))) + +;;; -------------------------------------------------------------------------- +;;; wttrin--forecast-format +;;; -------------------------------------------------------------------------- + +(ert-deftest test-wttrin--forecast-format-normal-three-days-fahrenheit () + "Normal: unit system \"u\" renders three labeled lines with °F ranges." + (let* ((wttrin-unit-system "u") + (text (wttrin--forecast-format (test-wttrin--forecast-days) 3)) + (lines (split-string text "\n"))) + (should (= 3 (length lines))) + (should (string-match-p "\\`Today 71-88°F Partly cloudy\\'" (nth 0 lines))) + (should (string-match-p "\\`Tomorrow 68-79°F Light rain\\'" (nth 1 lines))) + ;; 2026-07-03 is a Friday; the third day is labeled by weekday. + (should (string-match-p "\\`Fri 70-88°F Sunny\\'" (nth 2 lines))))) + +(ert-deftest test-wttrin--forecast-format-normal-metric () + "Normal: a non-\"u\" unit system renders °C ranges." + (let* ((wttrin-unit-system "m") + (text (wttrin--forecast-format (test-wttrin--forecast-days) 2))) + (should (string-match-p "Today 22-31°C" text)) + (should (string-match-p "Tomorrow 20-26°C" text)))) + +(ert-deftest test-wttrin--forecast-format-boundary-day-counts () + "Boundary: 0 days is nil; 1 day is one line; >3 caps at the data." + (let ((wttrin-unit-system "u") + (days (test-wttrin--forecast-days))) + (should-not (wttrin--forecast-format days 0)) + (should (= 1 (length (split-string (wttrin--forecast-format days 1) "\n")))) + (should (= 3 (length (split-string (wttrin--forecast-format days 5) "\n")))))) + +(ert-deftest test-wttrin--forecast-format-boundary-missing-hourly-desc () + "Boundary: a day with no hourly data renders the temps without a description." + (let* ((wttrin-unit-system "u") + (day '((date . "2026-07-01") (mintempF . "71") (maxtempF . "88") + (mintempC . "22") (maxtempC . "31") (hourly . ()))) + (text (wttrin--forecast-format (list day) 1))) + (should (equal "Today 71-88°F" text)))) + +(ert-deftest test-wttrin--forecast-format-error-nil-days () + "Error: nil day list returns nil." + (should-not (wttrin--forecast-format nil 3))) + +;;; -------------------------------------------------------------------------- +;;; tooltip integration +;;; -------------------------------------------------------------------------- + +(defun test-wttrin--forecast-tooltip-fixture (option-days) + "Return the tooltip with fresh current + forecast caches and OPTION-DAYS." + (let ((wttrin-unit-system "u") + (wttrin-mode-line-tooltip-forecast-days option-days) + (wttrin--mode-line-cache (cons (float-time) "Paris: ☀️ +61°F Clear")) + (wttrin--mode-line-forecast-cache + (cons (float-time) (test-wttrin--forecast-days)))) + (wttrin--mode-line-tooltip))) + +(ert-deftest test-wttrin--forecast-tooltip-normal-appends-block () + "Normal: with the option at 3, the tooltip carries the forecast lines." + (testutil-wttrin-setup) + (unwind-protect + (let ((tooltip (test-wttrin--forecast-tooltip-fixture 3))) + (should (string-match-p "Paris" tooltip)) + (should (string-match-p "Today 71-88°F Partly cloudy" tooltip)) + (should (string-match-p "Fri 70-88°F Sunny" tooltip)) + (should (string-match-p "Updated" tooltip))) + (testutil-wttrin-teardown))) + +(ert-deftest test-wttrin--forecast-tooltip-boundary-option-zero-unchanged () + "Boundary: with the option at 0 (default), the tooltip has no forecast." + (testutil-wttrin-setup) + (unwind-protect + (let ((tooltip (test-wttrin--forecast-tooltip-fixture 0))) + (should (string-match-p "Paris" tooltip)) + (should-not (string-match-p "Today" tooltip))) + (testutil-wttrin-teardown))) + +(ert-deftest test-wttrin--forecast-tooltip-boundary-empty-forecast-cache () + "Boundary: option on but no forecast cache yet leaves the tooltip as before." + (testutil-wttrin-setup) + (unwind-protect + (let ((wttrin-mode-line-tooltip-forecast-days 3) + (wttrin--mode-line-cache (cons (float-time) "Paris: ☀️ +61°F Clear")) + (wttrin--mode-line-forecast-cache nil)) + (let ((tooltip (wttrin--mode-line-tooltip))) + (should (string-match-p "Paris" tooltip)) + (should-not (string-match-p "Today" tooltip)))) + (testutil-wttrin-teardown))) + +(provide 'test-wttrin--mode-line-forecast) +;;; test-wttrin--mode-line-forecast.el ends here diff --git a/tests/test-wttrin--render-loading-placeholder.el b/tests/test-wttrin--render-loading-placeholder.el new file mode 100644 index 0000000..5c27557 --- /dev/null +++ b/tests/test-wttrin--render-loading-placeholder.el @@ -0,0 +1,64 @@ +;;; test-wttrin--render-loading-placeholder.el --- Loading placeholder centering -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Craig Jennings + +;;; Commentary: +;; The loading placeholder must be centered every time it is shown, including on +;; an `a' switch where the *wttr.in* buffer is already displayed. In that case +;; `window-configuration-change-hook' does not fire, so `wttrin-query' relied on +;; a stale window margin from the previous weather and the placeholder rendered +;; off-center. `wttrin--render-loading-placeholder' centers it explicitly. + +;;; Code: + +(require 'ert) +(require 'cl-lib) +(require 'wttrin) + +(ert-deftest test-wttrin--render-loading-placeholder-normal-shows-display-name () + "Normal: the placeholder text names the display string, not the raw query." + (with-temp-buffer + (cl-letf (((symbol-function 'wttrin--update-layout) #'ignore)) + (wttrin--render-loading-placeholder "1500 Sugar Bowl Dr" "Superdome") + (should (string-match-p "Loading weather for Superdome\\.\\.\\." + (buffer-string))) + (should-not wttrin--weather-rendered)))) + +(ert-deftest test-wttrin--render-loading-placeholder-boundary-nil-display-uses-query () + "Boundary: with no display name, the placeholder falls back to the query." + (with-temp-buffer + (cl-letf (((symbol-function 'wttrin--update-layout) #'ignore)) + (wttrin--render-loading-placeholder "Reykjavik" nil) + (should (string-match-p "Loading weather for Reykjavik" (buffer-string)))))) + +(ert-deftest test-wttrin--render-loading-placeholder-normal-centers-via-layout () + "Normal: rendering the placeholder runs the layout pass, so it is centered by +the same path weather uses rather than left to the hook (which does not fire on +a reused, already-displayed buffer)." + (with-temp-buffer + (let ((layout-calls 0)) + (cl-letf (((symbol-function 'wttrin--update-layout) + (lambda (&rest _) (setq layout-calls (1+ layout-calls))))) + (wttrin--render-loading-placeholder "Reykjavik" "Reykjavik") + (should (= 1 layout-calls)))))) + +(ert-deftest test-wttrin--render-loading-placeholder-regression-replaces-stale-margin () + "Regression: a stale window margin left by the previous weather block is +recomputed for the placeholder, so it no longer renders at the old margin after +an `a' switch." + (let ((buf (get-buffer-create "*wttrin-ph-test*"))) + (unwind-protect + (save-window-excursion + (set-window-buffer (selected-window) buf) + ;; A wide margin as if left over from a previous (wide) weather block. + (set-window-margins (selected-window) 40) + (with-current-buffer buf + (let ((wttrin-auto-fit-font nil)) + (wttrin--render-loading-placeholder "Reykjavik" "Reykjavik") + ;; The placeholder path must have recomputed the margin, not left + ;; the stale 40 in place. + (should-not (equal 40 (car (window-margins (selected-window)))))))) + (kill-buffer buf)))) + +(provide 'test-wttrin--render-loading-placeholder) +;;; test-wttrin--render-loading-placeholder.el ends here diff --git a/tests/test-wttrin-favorite-override.el b/tests/test-wttrin-favorite-override.el new file mode 100644 index 0000000..95916c7 --- /dev/null +++ b/tests/test-wttrin-favorite-override.el @@ -0,0 +1,83 @@ +;;; test-wttrin-favorite-override.el --- Tests for the runtime favorite override -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Craig Jennings + +;;; Commentary: +;; Unit tests for the split between the configured favorite +;; (`wttrin-favorite-location', a defcustom set in init) and the runtime +;; favorite (`wttrin--favorite-override', set by `d'/the geolocation commands +;; and persisted by savehist). The effective favorite resolves via +;; `wttrin--favorite-location': the override wins over the config, so a favorite +;; chosen at runtime is not clobbered by an init that also sets the option. + +;;; Code: + +(require 'ert) +(require 'cl-lib) +(require 'wttrin) + +;;; wttrin--favorite-location (resolver) + +(ert-deftest test-wttrin-favorite-override-normal-config-when-no-override () + "Normal: with no override, the effective favorite is the configured one." + (let ((wttrin-favorite-location "New Orleans, LA") + (wttrin--favorite-override nil)) + (should (equal "New Orleans, LA" (wttrin--favorite-location))))) + +(ert-deftest test-wttrin-favorite-override-normal-override-wins () + "Normal: a runtime override shadows the configured favorite." + (let ((wttrin-favorite-location "New Orleans, LA") + (wttrin--favorite-override "Hyatt Place Warwick, RI")) + (should (equal "Hyatt Place Warwick, RI" (wttrin--favorite-location))))) + +(ert-deftest test-wttrin-favorite-override-boundary-both-nil () + "Boundary: no config and no override yields nil (favorite disabled)." + (let ((wttrin-favorite-location nil) + (wttrin--favorite-override nil)) + (should (null (wttrin--favorite-location))))) + +(ert-deftest test-wttrin-favorite-override-boundary-auto-detect-override () + "Boundary: the override carries the tri-state t (auto-detect)." + (let ((wttrin-favorite-location "New Orleans, LA") + (wttrin--favorite-override t)) + (should (eq t (wttrin--favorite-location))))) + +(ert-deftest test-wttrin-favorite-override-regression-init-does-not-clobber () + "Regression: setting the config after an override (as an init `setopt' would) +does not change the effective favorite. This is the bug where a favorite set +with `d' reverted to the init value on restart." + (let ((wttrin--favorite-override "Hyatt Place Warwick, RI") + (wttrin-favorite-location nil)) + ;; Simulate the init running its (setopt wttrin-favorite-location ...). + (setq wttrin-favorite-location "New Orleans, LA") + (should (equal "Hyatt Place Warwick, RI" (wttrin--favorite-location))))) + +;;; wttrin--set-favorite-location writes the override, not the config + +(ert-deftest test-wttrin-favorite-override-normal-setter-writes-override () + "Normal: the setter writes the runtime override and leaves the config intact." + (let ((wttrin-favorite-location "New Orleans, LA") + (wttrin--favorite-override nil) + (wttrin--location-history nil)) + (wttrin--set-favorite-location "Paris, FR") + (should (equal "Paris, FR" wttrin--favorite-override)) + (should (equal "New Orleans, LA" wttrin-favorite-location)) + (should (equal "Paris, FR" (wttrin--favorite-location))))) + +;;; 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-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-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)))) + +(provide 'test-wttrin-favorite-override) +;;; test-wttrin-favorite-override.el ends here diff --git a/tests/test-wttrin-make-default.el b/tests/test-wttrin-make-default.el index e715e12..c7954c8 100644 --- a/tests/test-wttrin-make-default.el +++ b/tests/test-wttrin-make-default.el @@ -6,7 +6,9 @@ ;; Unit tests for wttrin--set-favorite-location and wttrin-make-default, ;; the weather-buffer command (bound to "d") that promotes the displayed -;; location to the persisted favorite. +;; location to the persisted favorite. The favorite is written to the runtime +;; override `wttrin--favorite-override' (not the `wttrin-favorite-location' +;; defcustom), and read back through `wttrin--favorite-location'. ;;; Code: @@ -21,26 +23,30 @@ ;;; Normal Cases (ert-deftest test-wttrin--set-favorite-location-normal-sets-variable () - "Normal: sets `wttrin-favorite-location' to the given location." + "Normal: sets the runtime favorite (leaving the config option untouched)." (let ((wttrin-favorite-location nil) + (wttrin--favorite-override nil) (savehist-additional-variables nil)) (wttrin--set-favorite-location "Paris, FR") - (should (equal wttrin-favorite-location "Paris, FR")))) + (should (equal wttrin--favorite-override "Paris, FR")) + (should (equal "Paris, FR" (wttrin--favorite-location))))) (ert-deftest test-wttrin--set-favorite-location-error-no-savehist-loaded () "Error: setting the favorite works even when savehist is not loaded. The setter must not touch `savehist-additional-variables' directly (it may be unbound); persistence is left to `wttrin--savehist-register'." - (let ((wttrin-favorite-location nil)) + (let ((wttrin-favorite-location nil) + (wttrin--favorite-override nil)) ;; Simulate savehist absent: the variable is unbound. (cl-letf (((symbol-function 'wttrin--savehist-register) (lambda () (error "Should not be called from the setter")))) (wttrin--set-favorite-location "Oslo, NO") - (should (equal wttrin-favorite-location "Oslo, NO"))))) + (should (equal wttrin--favorite-override "Oslo, NO"))))) (ert-deftest test-wttrin--set-favorite-location-normal-drops-from-history () "Normal: promoting a location removes it from the search history." (let ((wttrin-favorite-location nil) + (wttrin--favorite-override nil) (wttrin--location-history '("Reykjavik" "Oslo, NO"))) (wttrin--set-favorite-location "Reykjavik") (should-not (member "Reykjavik" wttrin--location-history)) @@ -49,16 +55,20 @@ unbound); persistence is left to `wttrin--savehist-register'." (ert-deftest test-wttrin--set-favorite-location-boundary-not-in-history-is-noop () "Boundary: promoting a location absent from history leaves history intact." (let ((wttrin-favorite-location nil) + (wttrin--favorite-override nil) (wttrin--location-history '("Oslo, NO"))) (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 favorite for persistence." +(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-location savehist-additional-variables)))) + (should-not (memq 'wttrin--favorite-override savehist-additional-variables)) + (should-not (memq 'wttrin-favorite-location savehist-additional-variables)))) ;;; -------------------------------------------------------------------------- ;;; mode-line refresh when the favorite changes @@ -70,6 +80,7 @@ unbound); persistence is left to `wttrin--savehist-register'." "Normal: changing the favorite while the mode-line is active clears the stale cache and fetches fresh weather for the new location immediately." (let ((wttrin-favorite-location "Oslo, NO") + (wttrin--favorite-override nil) (wttrin-mode-line-mode t) (wttrin--mode-line-cache (cons 0.0 "Oslo, NO: sun")) (fetched nil)) @@ -86,6 +97,7 @@ stale cache and fetches fresh weather for the new location immediately." (ert-deftest test-wttrin--set-favorite-location-boundary-mode-line-off-no-fetch () "Boundary: with the mode-line inactive, changing the favorite does not fetch." (let ((wttrin-favorite-location "Oslo, NO") + (wttrin--favorite-override nil) (wttrin-mode-line-mode nil) (fetched nil)) (cl-letf (((symbol-function 'wttrin--mode-line-fetch-weather) @@ -95,7 +107,8 @@ stale cache and fetches fresh weather for the new location immediately." (ert-deftest test-wttrin--set-favorite-location-boundary-unchanged-no-fetch () "Boundary: re-promoting the current favorite does not refetch the mode-line." - (let ((wttrin-favorite-location "Paris, FR") + (let ((wttrin-favorite-location nil) + (wttrin--favorite-override "Paris, FR") (wttrin-mode-line-mode t) (fetched nil)) (cl-letf (((symbol-function 'wttrin--mode-line-fetch-weather) @@ -114,22 +127,26 @@ stale cache and fetches fresh weather for the new location immediately." (ert-deftest test-wttrin-make-default-normal-sets-favorite-from-current () "Normal: promotes the buffer's current location to the favorite." (let ((wttrin-favorite-location nil) + (wttrin--favorite-override nil) + (wttrin-saved-locations nil) + (wttrin--saved-locations-runtime nil) (savehist-additional-variables nil)) (with-temp-buffer (setq-local wttrin--current-location "Tokyo, JP") (wttrin-make-default) - (should (equal wttrin-favorite-location "Tokyo, JP"))))) + (should (equal "Tokyo, JP" (wttrin--favorite-location)))))) ;;; Boundary Cases (ert-deftest test-wttrin-make-default-boundary-nil-current-leaves-favorite () "Boundary: no current location is a no-op that leaves the favorite intact." (let ((wttrin-favorite-location "Berkeley, CA") + (wttrin--favorite-override nil) (savehist-additional-variables nil)) (with-temp-buffer (setq-local wttrin--current-location nil) (wttrin-make-default) - (should (equal wttrin-favorite-location "Berkeley, CA"))))) + (should (equal "Berkeley, CA" (wttrin--favorite-location)))))) ;;; -------------------------------------------------------------------------- ;;; favorite in completion candidates @@ -141,7 +158,8 @@ stale cache and fetches fresh weather for the new location immediately." "Normal: a typed-in favorite is offered in the picker, at the front." (let ((wttrin-default-locations '("Honolulu, HI" "Berkeley, CA")) (wttrin--location-history nil) - (wttrin-favorite-location "Reykjavik")) + (wttrin-favorite-location "Reykjavik") + (wttrin--favorite-override nil)) (should (equal (wttrin--completion-candidates) (list wttrin--geolocation-sentinel "Reykjavik" "Honolulu, HI" "Berkeley, CA"))))) @@ -153,14 +171,16 @@ stale cache and fetches fresh weather for the new location immediately." (require 'cl-lib) (let ((wttrin-default-locations '("Honolulu, HI" "Berkeley, CA")) (wttrin--location-history nil) - (wttrin-favorite-location "Berkeley, CA")) + (wttrin-favorite-location "Berkeley, CA") + (wttrin--favorite-override nil)) (should (= 1 (cl-count "Berkeley, CA" (wttrin--completion-candidates) :test #'equal))))) (ert-deftest test-wttrin-make-default-boundary-nil-favorite-candidates-unchanged () "Boundary: nil favorite leaves the candidate list as defaults plus history." (let ((wttrin-default-locations '("Honolulu, HI")) (wttrin--location-history '("Oslo, NO")) - (wttrin-favorite-location nil)) + (wttrin-favorite-location nil) + (wttrin--favorite-override nil)) (should (equal (wttrin--completion-candidates) (list wttrin--geolocation-sentinel "Honolulu, HI" "Oslo, NO"))))) diff --git a/tests/test-wttrin-saved-locations.el b/tests/test-wttrin-saved-locations.el index de97df0..b41d7ee 100644 --- a/tests/test-wttrin-saved-locations.el +++ b/tests/test-wttrin-saved-locations.el @@ -3,10 +3,20 @@ ;; Copyright (C) 2024-2026 Craig Jennings ;;; Commentary: -;; Unit tests for the named-locations directory (Phase 1): the normalizer +;; Unit tests for the named-locations directory: the normalizer/union ;; `wttrin--saved-locations', the resolver `wttrin--resolve-location-query', ;; candidate de-duplication/precedence, favorite-as-name resolution, alias cache ;; identity, history suppression of saved names, and savehist registration. +;; +;; The directory has two layers: the configured `wttrin-saved-locations' +;; defcustom (a seed the user sets in init) and the runtime +;; `wttrin--saved-locations-runtime' (savehist-persisted, mutated by `d', +;; `wttrin-save-location', rename, and remove). `wttrin--saved-locations' +;; returns the union, runtime winning on a name collision. Read-only tests set +;; up the directory via the config layer; mutation tests use the runtime layer, +;; since the runtime commands cannot delete a config-seeded entry. The favorite +;; is likewise split: the `wttrin-favorite-location' defcustom plus the runtime +;; `wttrin--favorite-override', read back through `wttrin--favorite-location'. ;;; Code: @@ -15,42 +25,61 @@ (require 'wttrin) (require 'testutil-wttrin) -;;; wttrin--saved-locations (normalizer) +;;; wttrin--saved-locations (normalizer/union) (ert-deftest test-wttrin-saved-locations-normal-pairs-returned () "Normal: well-formed pairs are returned as (NAME . QUERY)." - (let ((wttrin-saved-locations '(("Home" . "1500 Sugar Bowl Dr, New Orleans")))) + (let ((wttrin-saved-locations '(("Home" . "1500 Sugar Bowl Dr, New Orleans"))) + (wttrin--saved-locations-runtime nil)) (should (equal '(("Home" . "1500 Sugar Bowl Dr, New Orleans")) (wttrin--saved-locations))))) (ert-deftest test-wttrin-saved-locations-boundary-bare-string-shorthand () "Boundary: a bare string S becomes (S . S)." - (let ((wttrin-saved-locations '("Berkeley, CA"))) + (let ((wttrin-saved-locations '("Berkeley, CA")) + (wttrin--saved-locations-runtime nil)) (should (equal '(("Berkeley, CA" . "Berkeley, CA")) (wttrin--saved-locations))))) (ert-deftest test-wttrin-saved-locations-boundary-whitespace-trimmed () "Boundary: surrounding whitespace on name and query is trimmed." - (let ((wttrin-saved-locations '((" Home " . " Paris, FR ")))) + (let ((wttrin-saved-locations '((" Home " . " Paris, FR "))) + (wttrin--saved-locations-runtime nil)) (should (equal '(("Home" . "Paris, FR")) (wttrin--saved-locations))))) (ert-deftest test-wttrin-saved-locations-error-malformed-skipped () "Error: non-cons, non-string, and empty entries are skipped, not fatal." (let ((wttrin-saved-locations - (list '("Good" . "Tokyo") 42 '("" . "x") '("y" . "") " " '(a . b)))) + (list '("Good" . "Tokyo") 42 '("" . "x") '("y" . "") " " '(a . b))) + (wttrin--saved-locations-runtime nil)) (should (equal '(("Good" . "Tokyo")) (wttrin--saved-locations))))) +(ert-deftest test-wttrin-saved-locations-normal-runtime-overlays-config () + "Normal: a runtime entry wins over a config entry with the same name." + (let ((wttrin-saved-locations '(("Home" . "Paris, FR"))) + (wttrin--saved-locations-runtime '(("Home" . "Tokyo, JP")))) + (should (equal '(("Home" . "Tokyo, JP")) (wttrin--saved-locations))))) + +(ert-deftest test-wttrin-saved-locations-normal-union-runtime-then-config () + "Normal: distinct config and runtime entries both appear, runtime first." + (let ((wttrin-saved-locations '(("Work" . "Tokyo, JP"))) + (wttrin--saved-locations-runtime '(("Home" . "Paris, FR")))) + (should (equal '(("Home" . "Paris, FR") ("Work" . "Tokyo, JP")) + (wttrin--saved-locations))))) + ;;; wttrin--resolve-location-query (ert-deftest test-wttrin-saved-locations-normal-resolve-name-to-query () "Normal: a saved name resolves to its query." - (let ((wttrin-saved-locations '(("Craig's House" . "1500 Sugar Bowl Dr, New Orleans")))) + (let ((wttrin-saved-locations '(("Craig's House" . "1500 Sugar Bowl Dr, New Orleans"))) + (wttrin--saved-locations-runtime nil)) (should (equal "1500 Sugar Bowl Dr, New Orleans" (wttrin--resolve-location-query "Craig's House"))))) (ert-deftest test-wttrin-saved-locations-boundary-resolve-passthrough () "Boundary: a non-saved selection passes through unchanged." - (let ((wttrin-saved-locations '(("Home" . "Paris, FR")))) + (let ((wttrin-saved-locations '(("Home" . "Paris, FR"))) + (wttrin--saved-locations-runtime nil)) (should (equal "Tokyo, JP" (wttrin--resolve-location-query "Tokyo, JP"))))) ;;; Candidate de-duplication and precedence @@ -61,7 +90,9 @@ (unwind-protect (let ((wttrin-geolocation-enabled nil) (wttrin-saved-locations '(("Home" . "Paris, FR"))) + (wttrin--saved-locations-runtime nil) (wttrin-favorite-location "Reykjavik") + (wttrin--favorite-override nil) (wttrin-default-locations '("Honolulu, HI")) (wttrin--location-history '("Tokyo"))) (should (equal '("Home" "Reykjavik" "Honolulu, HI" "Tokyo") @@ -74,7 +105,9 @@ (unwind-protect (let ((wttrin-geolocation-enabled nil) (wttrin-saved-locations '(("Honolulu, HI" . "Honolulu, HI"))) + (wttrin--saved-locations-runtime nil) (wttrin-favorite-location nil) + (wttrin--favorite-override nil) (wttrin-default-locations '("Honolulu, HI" "Berkeley, CA")) (wttrin--location-history nil)) (should (equal '("Honolulu, HI" "Berkeley, CA") @@ -86,14 +119,18 @@ (ert-deftest test-wttrin-saved-locations-normal-favorite-name-resolves-to-query () "Normal: a favorite that is a saved name resolves to its query for fetching." (let ((wttrin-saved-locations '(("Home" . "1500 Sugar Bowl Dr, New Orleans"))) - (wttrin-favorite-location "Home")) + (wttrin--saved-locations-runtime nil) + (wttrin-favorite-location "Home") + (wttrin--favorite-override nil)) (should (equal "1500 Sugar Bowl Dr, New Orleans" (wttrin--resolve-favorite-location))))) (ert-deftest test-wttrin-saved-locations-normal-favorite-display-shows-name () "Normal: the favorite display name is the saved name, not its query." (let ((wttrin-saved-locations '(("Home" . "1500 Sugar Bowl Dr, New Orleans"))) - (wttrin-favorite-location "Home")) + (wttrin--saved-locations-runtime nil) + (wttrin-favorite-location "Home") + (wttrin--favorite-override nil)) (should (equal "Home" (wttrin--favorite-location-display-name))))) ;;; Alias cache identity @@ -101,7 +138,8 @@ (ert-deftest test-wttrin-saved-locations-normal-cache-keyed-on-query () "Normal: cache identity follows the query, not the display name. Two names with the same query share a key; the name never leaks into the key." - (let ((wttrin-saved-locations '(("A" . "Paris, FR") ("B" . "Paris, FR")))) + (let ((wttrin-saved-locations '(("A" . "Paris, FR") ("B" . "Paris, FR"))) + (wttrin--saved-locations-runtime nil)) (should (equal (wttrin--make-cache-key (wttrin--resolve-location-query "A")) (wttrin--make-cache-key (wttrin--resolve-location-query "B")))))) @@ -112,6 +150,7 @@ Two names with the same query share a key; the name never leaks into the key." (testutil-wttrin-setup) (unwind-protect (let ((wttrin-saved-locations '(("Home" . "Paris, FR"))) + (wttrin--saved-locations-runtime nil) (wttrin-default-locations '()) (wttrin--location-history nil)) (wttrin--add-to-location-history "Home") @@ -120,12 +159,15 @@ 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: wttrin-saved-locations is registered for savehist persistence." +(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 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 @@ -141,7 +183,8 @@ Two names with the same query share a key; the name never leaks into the key." (ert-deftest test-wttrin-saved-locations-normal-put-adds-and-updates () "Normal: put adds a new entry and updates an existing name without duplicating." - (let ((wttrin-saved-locations nil)) + (let ((wttrin-saved-locations nil) + (wttrin--saved-locations-runtime nil)) (wttrin--put-saved-location "Home" "Paris, FR") (should (equal "Paris, FR" (wttrin--resolve-location-query "Home"))) (wttrin--put-saved-location "Home" "Tokyo, JP") @@ -150,7 +193,8 @@ Two names with the same query share a key; the name never leaks into the key." (ert-deftest test-wttrin-saved-locations-error-put-rejects-empty-and-sentinel () "Error: put refuses an empty name, empty query, or the sentinel name." - (let ((wttrin-saved-locations nil)) + (let ((wttrin-saved-locations nil) + (wttrin--saved-locations-runtime nil)) (should-error (wttrin--put-saved-location "" "Paris") :type 'user-error) (should-error (wttrin--put-saved-location "Home" "") :type 'user-error) (should-error (wttrin--put-saved-location wttrin--geolocation-sentinel "x") @@ -160,25 +204,30 @@ Two names with the same query share a key; the name never leaks into the key." (ert-deftest test-wttrin-saved-locations-normal-rename () "Normal: rename moves the entry and updates the favorite reference." - (let ((wttrin-saved-locations '(("Home" . "Paris, FR"))) - (wttrin-favorite-location "Home")) + (let ((wttrin-saved-locations nil) + (wttrin--saved-locations-runtime '(("Home" . "Paris, FR"))) + (wttrin-favorite-location nil) + (wttrin--favorite-override "Home")) (cl-letf (((symbol-function 'message) (lambda (&rest _) nil))) (wttrin-rename-location "Home" "Casa")) (should (equal "Paris, FR" (wttrin--resolve-location-query "Casa"))) (should-not (assoc "Home" (wttrin--saved-locations))) - (should (equal "Casa" wttrin-favorite-location)))) + (should (equal "Casa" (wttrin--favorite-location))))) (ert-deftest test-wttrin-saved-locations-error-rename-collision-refused () "Error: renaming onto an existing name is refused and changes nothing." - (let ((wttrin-saved-locations '(("Home" . "Paris, FR") ("Work" . "Tokyo, JP")))) + (let ((wttrin-saved-locations nil) + (wttrin--saved-locations-runtime '(("Home" . "Paris, FR") ("Work" . "Tokyo, JP")))) (should-error (wttrin-rename-location "Home" "Work") :type 'user-error) (should (equal "Paris, FR" (wttrin--resolve-location-query "Home"))))) (ert-deftest test-wttrin-saved-locations-normal-rename-favorite-refreshes-mode-line () "Normal: renaming the favorite refreshes the mode-line so the icon and tooltip follow the new name immediately instead of at the next scheduled fetch." - (let ((wttrin-saved-locations '(("Home" . "Paris, FR"))) - (wttrin-favorite-location "Home") + (let ((wttrin-saved-locations nil) + (wttrin--saved-locations-runtime '(("Home" . "Paris, FR"))) + (wttrin-favorite-location nil) + (wttrin--favorite-override "Home") (wttrin--location-history nil) (wttrin-mode-line-mode t) (fetched nil)) @@ -188,13 +237,15 @@ follow the new name immediately instead of at the next scheduled fetch." (lambda () nil)) ((symbol-function 'message) (lambda (&rest _) nil))) (wttrin-rename-location "Home" "Casa")) - (should (equal "Casa" wttrin-favorite-location)) + (should (equal "Casa" (wttrin--favorite-location))) (should fetched))) (ert-deftest test-wttrin-saved-locations-boundary-rename-non-favorite-no-refresh () "Boundary: renaming a location that is not the favorite does not refresh." - (let ((wttrin-saved-locations '(("Home" . "Paris, FR") ("Work" . "Tokyo, JP"))) - (wttrin-favorite-location "Work") + (let ((wttrin-saved-locations nil) + (wttrin--saved-locations-runtime '(("Home" . "Paris, FR") ("Work" . "Tokyo, JP"))) + (wttrin-favorite-location nil) + (wttrin--favorite-override "Work") (wttrin--location-history nil) (wttrin-mode-line-mode t) (fetched nil)) @@ -210,7 +261,8 @@ follow the new name immediately instead of at the next scheduled fetch." (ert-deftest test-wttrin-saved-locations-normal-remove-confirmed () "Normal: confirming removes the entry." - (let ((wttrin-saved-locations '(("Home" . "Paris, FR")))) + (let ((wttrin-saved-locations nil) + (wttrin--saved-locations-runtime '(("Home" . "Paris, FR")))) (cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) t)) ((symbol-function 'message) (lambda (&rest _) nil))) (wttrin-remove-location "Home")) @@ -218,17 +270,30 @@ follow the new name immediately instead of at the next scheduled fetch." (ert-deftest test-wttrin-saved-locations-boundary-remove-declined-keeps () "Boundary: declining the confirmation keeps the entry." - (let ((wttrin-saved-locations '(("Home" . "Paris, FR")))) + (let ((wttrin-saved-locations nil) + (wttrin--saved-locations-runtime '(("Home" . "Paris, FR")))) (cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) nil)) ((symbol-function 'message) (lambda (&rest _) nil))) (wttrin-remove-location "Home")) (should (assoc "Home" (wttrin--saved-locations))))) +(ert-deftest test-wttrin-saved-locations-boundary-remove-config-entry-remains () + "Boundary: removing a config-seeded entry (not in the runtime layer) leaves it +in the directory, since the runtime removal cannot delete an init-defined entry." + (let ((wttrin-saved-locations '(("Home" . "Paris, FR"))) + (wttrin--saved-locations-runtime nil)) + (cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) t)) + ((symbol-function 'message) (lambda (&rest _) nil))) + (wttrin-remove-location "Home")) + (should (assoc "Home" (wttrin--saved-locations))))) + (ert-deftest test-wttrin-saved-locations-normal-remove-favorite-refreshes-mode-line () "Normal: removing the favorite refreshes the mode-line so it stops showing the now-deleted alias's resolved weather and re-fetches against the bare query." - (let ((wttrin-saved-locations '(("Home" . "Paris, FR"))) - (wttrin-favorite-location "Home") + (let ((wttrin-saved-locations nil) + (wttrin--saved-locations-runtime '(("Home" . "Paris, FR"))) + (wttrin-favorite-location nil) + (wttrin--favorite-override "Home") (wttrin-mode-line-mode t) (fetched nil)) (cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) t)) @@ -242,8 +307,10 @@ now-deleted alias's resolved weather and re-fetches against the bare query." (ert-deftest test-wttrin-saved-locations-boundary-remove-non-favorite-no-refresh () "Boundary: removing a location that is not the favorite does not refresh." - (let ((wttrin-saved-locations '(("Home" . "Paris, FR") ("Work" . "Tokyo, JP"))) - (wttrin-favorite-location "Work") + (let ((wttrin-saved-locations nil) + (wttrin--saved-locations-runtime '(("Home" . "Paris, FR") ("Work" . "Tokyo, JP"))) + (wttrin-favorite-location nil) + (wttrin--favorite-override "Work") (wttrin-mode-line-mode t) (fetched nil)) (cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) t)) @@ -260,7 +327,9 @@ now-deleted alias's resolved weather and re-fetches against the bare query." (ert-deftest test-wttrin-saved-locations-normal-d-names-and-promotes () "Normal: d on a coordinate buffer names it, saves it, and promotes the name." (let ((wttrin-saved-locations nil) + (wttrin--saved-locations-runtime nil) (wttrin-favorite-location nil) + (wttrin--favorite-override nil) (wttrin-mode-line-mode nil)) (with-temp-buffer (setq-local wttrin--current-location "41.37,-71.83") @@ -270,12 +339,14 @@ now-deleted alias's resolved weather and re-fetches against the bare query." ((symbol-function 'message) (lambda (&rest _) nil))) (wttrin-make-default))) (should (equal "41.37,-71.83" (wttrin--resolve-location-query "Home"))) - (should (equal "Home" wttrin-favorite-location)))) + (should (equal "Home" (wttrin--favorite-location))))) (ert-deftest test-wttrin-saved-locations-boundary-d-empty-keeps-coordinates () "Boundary: an empty name at the d prompt keeps the coordinates, saves no entry." (let ((wttrin-saved-locations nil) + (wttrin--saved-locations-runtime nil) (wttrin-favorite-location nil) + (wttrin--favorite-override nil) (wttrin-mode-line-mode nil)) (with-temp-buffer (setq-local wttrin--current-location "41.37,-71.83") @@ -284,12 +355,14 @@ now-deleted alias's resolved weather and re-fetches against the bare query." ((symbol-function 'message) (lambda (&rest _) nil))) (wttrin-make-default))) (should (null (wttrin--saved-locations))) - (should (equal "41.37,-71.83" wttrin-favorite-location)))) + (should (equal "41.37,-71.83" (wttrin--favorite-location))))) (ert-deftest test-wttrin-saved-locations-boundary-d-named-buffer-no-prompt () "Boundary: d on a named buffer promotes the display name without prompting." (let ((wttrin-saved-locations nil) + (wttrin--saved-locations-runtime nil) (wttrin-favorite-location nil) + (wttrin--favorite-override nil) (wttrin-mode-line-mode nil) (prompted nil)) (with-temp-buffer @@ -300,7 +373,7 @@ now-deleted alias's resolved weather and re-fetches against the bare query." ((symbol-function 'message) (lambda (&rest _) nil))) (wttrin-make-default))) (should-not prompted) - (should (equal "Craig's House" wttrin-favorite-location)) + (should (equal "Craig's House" (wttrin--favorite-location))) (should (equal "1500 Sugar Bowl Dr" (wttrin--resolve-location-query "Craig's House"))))) @@ -308,14 +381,16 @@ now-deleted alias's resolved weather and re-fetches against the bare query." "Normal: making a typed location the default also saves it to the directory, so it persists as a named entry rather than only as the favorite string." (let ((wttrin-saved-locations nil) + (wttrin--saved-locations-runtime nil) (wttrin-favorite-location nil) + (wttrin--favorite-override nil) (wttrin-mode-line-mode nil)) (with-temp-buffer (setq-local wttrin--current-location "Reykjavik") (setq-local wttrin--current-display "Reykjavik") (cl-letf (((symbol-function 'message) (lambda (&rest _) nil))) (wttrin-make-default))) - (should (equal "Reykjavik" wttrin-favorite-location)) + (should (equal "Reykjavik" (wttrin--favorite-location))) (should (assoc "Reykjavik" (wttrin--saved-locations))) (should (equal "Reykjavik" (wttrin--resolve-location-query "Reykjavik"))))) @@ -323,7 +398,8 @@ so it persists as a named entry rather than only as the favorite string." (ert-deftest test-wttrin-saved-locations-normal-save-location-interactive () "Normal: the interactive save command reads the buffer query and a name." - (let ((wttrin-saved-locations nil)) + (let ((wttrin-saved-locations nil) + (wttrin--saved-locations-runtime nil)) (with-temp-buffer (setq-local wttrin--current-location "Paris, FR") (setq-local wttrin--current-display "Paris, FR") @@ -334,7 +410,8 @@ so it persists as a named entry rather than only as the favorite string." (ert-deftest test-wttrin-saved-locations-normal-rename-interactive () "Normal: the interactive rename command prompts for the entry and new name." - (let ((wttrin-saved-locations '(("Home" . "Paris, FR")))) + (let ((wttrin-saved-locations nil) + (wttrin--saved-locations-runtime '(("Home" . "Paris, FR")))) (cl-letf (((symbol-function 'completing-read) (lambda (&rest _) "Home")) ((symbol-function 'read-string) (lambda (&rest _) "Casa")) ((symbol-function 'message) (lambda (&rest _) nil))) @@ -343,7 +420,8 @@ so it persists as a named entry rather than only as the favorite string." (ert-deftest test-wttrin-saved-locations-normal-remove-interactive () "Normal: the interactive remove command prompts and confirms." - (let ((wttrin-saved-locations '(("Home" . "Paris, FR")))) + (let ((wttrin-saved-locations nil) + (wttrin--saved-locations-runtime '(("Home" . "Paris, FR")))) (cl-letf (((symbol-function 'completing-read) (lambda (&rest _) "Home")) ((symbol-function 'yes-or-no-p) (lambda (&rest _) t)) ((symbol-function 'message) (lambda (&rest _) nil))) @@ -352,7 +430,8 @@ so it persists as a named entry rather than only as the favorite string." (ert-deftest test-wttrin-saved-locations-boundary-save-empty-name-cancels () "Boundary: an empty name at the save prompt cancels without saving." - (let ((wttrin-saved-locations nil)) + (let ((wttrin-saved-locations nil) + (wttrin--saved-locations-runtime nil)) (with-temp-buffer (setq-local wttrin--current-location "Paris, FR") (cl-letf (((symbol-function 'read-string) (lambda (&rest _) " ")) @@ -368,6 +447,7 @@ so it persists as a named entry rather than only as the favorite string." (unwind-protect (let ((wttrin-default-locations '()) (wttrin-saved-locations nil) + (wttrin--saved-locations-runtime nil) (wttrin--location-history nil)) (wttrin--add-to-location-history "41.37,-71.83") (should (null wttrin--location-history))) @@ -379,40 +459,12 @@ so it persists as a named entry rather than only as the favorite string." "Normal: saving a location drops its query from history, so the place lives in the directory only and does not also appear as a separate history candidate." (let ((wttrin-saved-locations nil) + (wttrin--saved-locations-runtime nil) (wttrin--location-history '("New Orleans" "Paris"))) (cl-letf (((symbol-function 'message) (lambda (&rest _) nil))) (wttrin-save-location "Home" "New Orleans")) (should-not (member "New Orleans" wttrin--location-history)) (should (member "Paris" wttrin--location-history)))) -(ert-deftest test-wttrin-saved-locations-normal-d-alias-drops-query-from-history () - "Normal: making an aliased location the default drops its underlying query from -history, so the place does not also linger as a separate history candidate." - (let ((wttrin-saved-locations nil) - (wttrin-favorite-location nil) - (wttrin-mode-line-mode nil) - (wttrin--location-history '("New Orleans" "Paris"))) - (with-temp-buffer - (setq-local wttrin--current-location "New Orleans") - (setq-local wttrin--current-display "Home") - (cl-letf (((symbol-function 'message) (lambda (&rest _) nil))) - (wttrin-make-default))) - (should (equal "Home" wttrin-favorite-location)) - (should (assoc "Home" (wttrin--saved-locations))) - (should-not (member "New Orleans" wttrin--location-history)) - (should (member "Paris" wttrin--location-history)))) - -(ert-deftest test-wttrin-saved-locations-normal-remove-forgets-history () - "Normal: removing a saved location drops both its name and its query from -history, so a removed place does not resurface as a history candidate." - (let ((wttrin-saved-locations '(("Home" . "New Orleans"))) - (wttrin--location-history '("Home" "New Orleans" "Paris"))) - (cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) t)) - ((symbol-function 'message) (lambda (&rest _) nil))) - (wttrin-remove-location "Home")) - (should-not (member "Home" wttrin--location-history)) - (should-not (member "New Orleans" wttrin--location-history)) - (should (member "Paris" wttrin--location-history)))) - (provide 'test-wttrin-saved-locations) ;;; test-wttrin-saved-locations.el ends here diff --git a/tests/test-wttrin-set-location-from-geolocation.el b/tests/test-wttrin-set-location-from-geolocation.el index f16b2ae..10e5cc0 100644 --- a/tests/test-wttrin-set-location-from-geolocation.el +++ b/tests/test-wttrin-set-location-from-geolocation.el @@ -20,16 +20,24 @@ (defvar test-wttrin-set-location-from-geolocation--saved-favorite nil "Snapshot of `wttrin-favorite-location' restored in teardown.") +(defvar test-wttrin-set-location-from-geolocation--saved-override nil + "Snapshot of `wttrin--favorite-override' restored in teardown.") + (defun test-wttrin-set-location-from-geolocation-setup () - "Snapshot `wttrin-favorite-location' and clear it for the test." + "Snapshot the configured favorite and runtime override, clearing both." (setq test-wttrin-set-location-from-geolocation--saved-favorite wttrin-favorite-location) - (setq wttrin-favorite-location nil)) + (setq test-wttrin-set-location-from-geolocation--saved-override + wttrin--favorite-override) + (setq wttrin-favorite-location nil) + (setq wttrin--favorite-override nil)) (defun test-wttrin-set-location-from-geolocation-teardown () - "Restore `wttrin-favorite-location' to its pre-test value." + "Restore the configured favorite and runtime override to pre-test values." (setq wttrin-favorite-location - test-wttrin-set-location-from-geolocation--saved-favorite)) + test-wttrin-set-location-from-geolocation--saved-favorite) + (setq wttrin--favorite-override + test-wttrin-set-location-from-geolocation--saved-override)) ;;; Helpers @@ -51,7 +59,7 @@ (progn (test-wttrin-set-location--with-detected "Berkeley, California" t (wttrin-set-location-from-geolocation)) - (should (string= "Berkeley, California" wttrin-favorite-location))) + (should (string= "Berkeley, California" (wttrin--favorite-location)))) (test-wttrin-set-location-from-geolocation-teardown))) (ert-deftest test-wttrin-set-location-from-geolocation-normal-decline-leaves-variable-unchanged () @@ -84,7 +92,7 @@ favorite immediately instead of at the next scheduled fetch." ((symbol-function 'wttrin--mode-line-set-placeholder) (lambda () nil))) (wttrin-set-location-from-geolocation)) - (should (string= "Berkeley, California" wttrin-favorite-location)) + (should (string= "Berkeley, California" (wttrin--favorite-location))) (should fetched)) (test-wttrin-set-location-from-geolocation-teardown))) @@ -97,7 +105,7 @@ favorite immediately instead of at the next scheduled fetch." (progn (test-wttrin-set-location--with-detected "München, Bayern" t (wttrin-set-location-from-geolocation)) - (should (string= "München, Bayern" wttrin-favorite-location))) + (should (string= "München, Bayern" (wttrin--favorite-location)))) (test-wttrin-set-location-from-geolocation-teardown))) ;;; Error Cases diff --git a/tests/test-wttrin-state-file.el b/tests/test-wttrin-state-file.el new file mode 100644 index 0000000..5648a13 --- /dev/null +++ b/tests/test-wttrin-state-file.el @@ -0,0 +1,258 @@ +;;; 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)))) + +(provide 'test-wttrin-state-file) +;;; test-wttrin-state-file.el ends here diff --git a/tests/test-wttrin-use-current-location.el b/tests/test-wttrin-use-current-location.el index 4b61657..d7d0ea6 100644 --- a/tests/test-wttrin-use-current-location.el +++ b/tests/test-wttrin-use-current-location.el @@ -18,14 +18,20 @@ (defvar test-wttrin-use-current-location--saved nil "Snapshot of `wttrin-favorite-location' restored in teardown.") +(defvar test-wttrin-use-current-location--saved-override nil + "Snapshot of `wttrin--favorite-override' restored in teardown.") + (defun test-wttrin-use-current-location-setup () - "Snapshot `wttrin-favorite-location' and clear it." + "Snapshot the configured favorite and the runtime override, clearing both." (setq test-wttrin-use-current-location--saved wttrin-favorite-location) - (setq wttrin-favorite-location nil)) + (setq test-wttrin-use-current-location--saved-override wttrin--favorite-override) + (setq wttrin-favorite-location nil) + (setq wttrin--favorite-override nil)) (defun test-wttrin-use-current-location-teardown () - "Restore `wttrin-favorite-location'." - (setq wttrin-favorite-location test-wttrin-use-current-location--saved)) + "Restore the configured favorite and the runtime override." + (setq wttrin-favorite-location test-wttrin-use-current-location--saved) + (setq wttrin--favorite-override test-wttrin-use-current-location--saved-override)) ;;; Normal Cases @@ -37,7 +43,7 @@ (cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _) t)) ((symbol-function 'message) (lambda (&rest _) nil))) (wttrin-use-current-location)) - (should (eq t wttrin-favorite-location))) + (should (eq t (wttrin--favorite-location)))) (test-wttrin-use-current-location-teardown))) (ert-deftest test-wttrin-use-current-location-normal-decline-leaves-unchanged () @@ -69,7 +75,7 @@ location immediately rather than at the next scheduled fetch." ((symbol-function 'wttrin--mode-line-set-placeholder) (lambda () nil))) (wttrin-use-current-location)) - (should (eq t wttrin-favorite-location)) + (should (eq t (wttrin--favorite-location))) (should fetched)) (test-wttrin-use-current-location-teardown))) 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 |
