1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
|
;;; test-wttrin-geolocation-command.el --- Tests for the external-command geolocation provider -*- lexical-binding: t; -*-
;; Copyright (C) 2024-2026 Craig Jennings
;;; Commentary:
;; Unit tests for the generic external-command geolocation provider:
;; `wttrin-geolocation--parse-coordinates' (pure JSON -> "LAT,LNG"), the
;; routing/fallback in `wttrin-geolocation-detect' when
;; `wttrin-geolocation-command' is set, and an end-to-end run of
;; `wttrin-geolocation--detect-via-command' against a real shell command.
;;; Code:
(require 'ert)
(require 'cl-lib)
(require 'wttrin)
(require 'wttrin-geolocation)
;;; wttrin-geolocation--parse-coordinates
(ert-deftest test-wttrin-geolocation-command-normal-parse-coordinates ()
"Normal: numeric lat/lng become a \"LAT,LNG\" string."
(should (equal "41.32,-71.81"
(wttrin-geolocation--parse-coordinates
"{\"lat\": 41.32, \"lng\": -71.81}"))))
(ert-deftest test-wttrin-geolocation-command-boundary-parse-ignores-extra-keys ()
"Boundary: extra keys (accuracy, address) are ignored."
(should (equal "1.5,2.5"
(wttrin-geolocation--parse-coordinates
"{\"lat\":1.5,\"lng\":2.5,\"accuracy_m\":11.5,\"address\":\"x\"}"))))
(ert-deftest test-wttrin-geolocation-command-error-parse-malformed-json ()
"Error: malformed JSON returns nil."
(should (null (wttrin-geolocation--parse-coordinates "not json")))
(should (null (wttrin-geolocation--parse-coordinates "")))
(should (null (wttrin-geolocation--parse-coordinates nil))))
(ert-deftest test-wttrin-geolocation-command-error-parse-missing-or-nonnumeric ()
"Error: missing or non-numeric coordinates return nil."
(should (null (wttrin-geolocation--parse-coordinates "{\"lat\":1.0}")))
(should (null (wttrin-geolocation--parse-coordinates
"{\"lat\":\"x\",\"lng\":\"y\"}"))))
;;; wttrin-geolocation--parse-address
(ert-deftest test-wttrin-geolocation-command-normal-parse-address ()
"Normal: the address key is returned verbatim."
(should (equal "Westerly, Rhode Island, USA"
(wttrin-geolocation--parse-address
"{\"lat\":1.0,\"lng\":2.0,\"address\":\"Westerly, Rhode Island, USA\"}"))))
(ert-deftest test-wttrin-geolocation-command-boundary-parse-address-label-synonym ()
"Boundary: a `label' key is accepted when `address' is absent."
(should (equal "Westerly, RI"
(wttrin-geolocation--parse-address
"{\"lat\":1.0,\"lng\":2.0,\"label\":\"Westerly, RI\"}"))))
(ert-deftest test-wttrin-geolocation-command-error-parse-address-absent ()
"Error: no address or label, malformed JSON, or empty string returns nil."
(should (null (wttrin-geolocation--parse-address "{\"lat\":1.0,\"lng\":2.0}")))
(should (null (wttrin-geolocation--parse-address "not json")))
(should (null (wttrin-geolocation--parse-address
"{\"lat\":1.0,\"lng\":2.0,\"address\":\"\"}"))))
;;; wttrin-geolocation-detect — routing and fallback
(ert-deftest test-wttrin-geolocation-command-normal-detect-uses-command ()
"Normal: with a command set that succeeds, its coordinates are used."
(let ((got 'none)
(ip-called nil)
(wttrin-geolocation-command "ignored-in-mock"))
(cl-letf (((symbol-function 'wttrin-geolocation--detect-via-command)
(lambda (cb) (funcall cb "1.0,2.0")))
((symbol-function 'wttrin-geolocation--detect-via-ip)
(lambda (cb) (setq ip-called t) (funcall cb "City, ST"))))
(wttrin-geolocation-detect (lambda (r &optional _a) (setq got r))))
(should (equal "1.0,2.0" got))
(should-not ip-called)))
(ert-deftest test-wttrin-geolocation-command-boundary-detect-falls-back-to-ip ()
"Boundary: with a command that fails (nil), detection falls back to IP."
(let ((got 'none)
(wttrin-geolocation-command "ignored-in-mock"))
(cl-letf (((symbol-function 'wttrin-geolocation--detect-via-command)
(lambda (cb) (funcall cb nil)))
((symbol-function 'wttrin-geolocation--detect-via-ip)
(lambda (cb) (funcall cb "City, ST"))))
(wttrin-geolocation-detect (lambda (r &optional _a) (setq got r))))
(should (equal "City, ST" got))))
(ert-deftest test-wttrin-geolocation-command-boundary-detect-no-command-uses-ip ()
"Boundary: with no command set, the IP path runs directly."
(let ((got 'none)
(cmd-called nil)
(wttrin-geolocation-command nil))
(cl-letf (((symbol-function 'wttrin-geolocation--detect-via-command)
(lambda (cb) (setq cmd-called t) (funcall cb "9.0,9.0")))
((symbol-function 'wttrin-geolocation--detect-via-ip)
(lambda (cb) (funcall cb "City, ST"))))
(wttrin-geolocation-detect (lambda (r &optional _a) (setq got r))))
(should (equal "City, ST" got))
(should-not cmd-called)))
;;; wttrin-geolocation--detect-via-command — real process
(defun test-wttrin-geolocation-command--run-sync (command)
"Run `wttrin-geolocation--detect-via-command' with COMMAND, wait, return coords.
The address (second callback argument) is ignored here; a separate test covers it."
(let ((result 'pending)
(wttrin-geolocation-command command))
(wttrin-geolocation--detect-via-command
(lambda (r &optional _a) (setq result r)))
(with-timeout (5 (error "detect-via-command timed out"))
(while (eq result 'pending)
(accept-process-output nil 0.05)))
result))
(ert-deftest test-wttrin-geolocation-command-integration-real-command-success ()
"Integration: a command printing lat/lng JSON resolves to \"LAT,LNG\".
Components: wttrin-geolocation--detect-via-command (real make-process),
the shell (real), wttrin-geolocation--parse-coordinates (real)."
(should (equal "1.5,2.5"
(test-wttrin-geolocation-command--run-sync
"echo '{\"lat\":1.5,\"lng\":2.5}'"))))
(ert-deftest test-wttrin-geolocation-command-integration-real-command-passes-address ()
"Integration: a command printing lat/lng plus an address passes both to the callback.
Components: detect-via-command (real make-process), the shell, the coord and
address parsers (real)."
(let ((coords 'pending)
(address 'pending)
(wttrin-geolocation-command
"echo '{\"lat\":1.5,\"lng\":2.5,\"address\":\"Westerly, RI\"}'"))
(wttrin-geolocation--detect-via-command
(lambda (c &optional a) (setq coords c address a)))
(with-timeout (5 (error "detect-via-command timed out"))
(while (eq coords 'pending)
(accept-process-output nil 0.05)))
(should (equal "1.5,2.5" coords))
(should (equal "Westerly, RI" address))))
(ert-deftest test-wttrin-geolocation-command-integration-real-command-nonzero-exit ()
"Integration: a command exiting non-zero yields nil (caller falls back to IP)."
(should (null (test-wttrin-geolocation-command--run-sync "exit 1"))))
(ert-deftest test-wttrin-geolocation-command-integration-real-command-bad-output ()
"Integration: a zero-exit command printing non-JSON yields nil."
(should (null (test-wttrin-geolocation-command--run-sync "echo not-json"))))
(ert-deftest test-wttrin-geolocation-command-error-spawn-failure-yields-nil ()
"Error: when the process cannot spawn, the callback receives nil."
(let ((result 'pending)
(wttrin-geolocation-command "whatever"))
(cl-letf (((symbol-function 'make-process)
(lambda (&rest _) (error "spawn failed"))))
(wttrin-geolocation--detect-via-command (lambda (r) (setq result r))))
(should (null result))))
(provide 'test-wttrin-geolocation-command)
;;; test-wttrin-geolocation-command.el ends here
|