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
|
-- Checks for lib/weather.lua.
-- Run from the repo root: lua test/test_weather.lua
-- The domain tables are what silently misreport when a boundary is off by one,
-- so every range boundary gets an assertion. The card itself is verified by
-- screenshot.
package.path = './?.lua;' .. package.path
local weather = require 'lib.weather'
-- === Condition id to icon =================================================
-- OWM ids group by hundreds, but the boundaries are not round numbers: the
-- ranges come from the polybar script that ran against this API for years.
-- Each assertion pairs the last id in a range with the first id of the next,
-- which is where an off-by-one would hide.
-- Sunrise 1500, sunset 1900, so DAY must sit INSIDE that window and NIGHT
-- outside it. Naming them without checking them against the window is how the
-- first draft of this test made every timestamp evaluate as night.
local DAY, NIGHT = 1700, 2000
local function icon(id, now)
return weather.icon(id, now, 1500, 1900) -- sunrise 1500, sunset 1900
end
-- Thunderstorm: everything up to 232.
assert(icon(200, DAY) == weather.ICON.thunder_day, 'id 200 day')
assert(icon(232, DAY) == weather.ICON.thunder_day, 'id 232 is still thunder')
assert(icon(232, NIGHT) == weather.ICON.thunder_night, 'id 232 night')
-- Light drizzle: 233..311.
assert(icon(233, DAY) == weather.ICON.drizzle_day, 'id 233 leaves thunder')
assert(icon(311, DAY) == weather.ICON.drizzle_day, 'id 311 is still light drizzle')
-- Heavy drizzle: 312..321.
assert(icon(312, DAY) == weather.ICON.drizzle_heavy_day, 'id 312 is heavy drizzle')
assert(icon(321, DAY) == weather.ICON.drizzle_heavy_day, 'id 321 is still heavy drizzle')
-- Rain: 322..531.
assert(icon(322, DAY) == weather.ICON.rain_day, 'id 322 is rain')
assert(icon(531, DAY) == weather.ICON.rain_day, 'id 531 is still rain')
-- Snow: 532..622. One icon, no day/night variant.
assert(icon(600, DAY) == weather.ICON.snow, 'id 600 is snow')
assert(icon(622, NIGHT) == weather.ICON.snow, 'snow is the same at night')
-- Fog: 623..771.
assert(icon(741, DAY) == weather.ICON.fog, 'id 741 is fog')
assert(icon(771, DAY) == weather.ICON.fog, 'id 771 is still fog')
-- Tornado is a single id, not a range.
assert(icon(781, DAY) == weather.ICON.tornado, 'id 781 is tornado')
-- Clear and few clouds each have a day and a night face.
assert(icon(800, DAY) == weather.ICON.clear_day, 'id 800 day is the sun')
assert(icon(800, NIGHT) == weather.ICON.clear_night, 'id 800 night is the moon')
assert(icon(801, DAY) == weather.ICON.few_day, 'id 801 day')
assert(icon(801, NIGHT) == weather.ICON.few_night, 'id 801 night')
-- Overcast: 802..804, no night variant.
assert(icon(804, DAY) == weather.ICON.overcast, 'id 804 is overcast')
-- Anything outside the known ids must be visibly wrong, not silently sunny.
assert(icon(999, DAY) == weather.ICON.unknown, 'unknown id gets the error glyph')
assert(weather.icon(nil, DAY, 1500, 1900) == weather.ICON.unknown, 'nil id')
-- Nothing may raise. A missing timestamp with a known sun, or a known
-- timestamp with no sun, both have to return a glyph rather than an error,
-- because an error here is a blank dashboard with no message.
assert(weather.icon(800, nil, 1500, 1900) == weather.ICON.clear_day,
'a nil now must not raise, and falls back to the day face')
assert(weather.icon(800, DAY, nil, nil) == weather.ICON.clear_day,
'no sun times must not raise')
-- The codepoint VALUES are deliberately not asserted. A test comparing
-- M.ICON.snow against a literal would only prove the same escape was typed
-- twice; whether E31A actually draws a snow cloud in the target font is not
-- something an assertion can see. That check is the glyph sheet rendered in
-- the plan's Step 5, and it is how a wrong-but-present codepoint was caught
-- before this file existed.
-- === Wind =================================================================
-- OWM metric gives m/s; the card shows km/h. The conversion is where a wrong
-- factor would look plausible but read 3.6x off.
assert(weather.kmh(10) == 36, '10 m/s is 36 km/h')
assert(weather.kmh(0) == 0, 'calm')
assert(weather.kmh(nil) == nil, 'missing wind speed stays missing')
-- Beaufort thresholds, in km/h, from the polybar script. Each assertion sits
-- on a boundary and just past it, which is where an inclusive/exclusive
-- mistake hides.
assert(weather.beaufort(0) == 0, 'calm is force 0')
assert(weather.beaufort(1) == 0, '1 km/h is still force 0')
assert(weather.beaufort(2) == 1, 'just over 1 is force 1')
assert(weather.beaufort(5) == 1, '5 is still force 1')
assert(weather.beaufort(6) == 2, 'just over 5 is force 2')
assert(weather.beaufort(11) == 2, '11 is still force 2')
assert(weather.beaufort(12) == 3, 'just over 11 is force 3')
assert(weather.beaufort(19) == 3, '19 is still force 3')
assert(weather.beaufort(28) == 4, '28 is force 4')
assert(weather.beaufort(38) == 5, '38 is force 5')
assert(weather.beaufort(49) == 6, '49 is force 6')
assert(weather.beaufort(61) == 7, '61 is force 7')
assert(weather.beaufort(74) == 8, '74 is force 8')
assert(weather.beaufort(88) == 9, '88 is force 9')
assert(weather.beaufort(102) == 10, '102 is force 10')
assert(weather.beaufort(117) == 11, '117 is force 11')
assert(weather.beaufort(118) == 12, 'over 117 is hurricane force')
-- Compass: eight points, each spanning 45 degrees, offset by half a step so
-- north straddles 0 rather than starting at it. The wraparound is the case
-- that a naive floor(deg/45) gets wrong.
assert(weather.compass(0) == 'N', '0 is north')
assert(weather.compass(360) == 'N', '360 wraps to north')
assert(weather.compass(11) == 'N', 'just under the NE boundary')
assert(weather.compass(349) == 'N', 'just over the NW boundary wraps to north')
assert(weather.compass(45) == 'NE', '45 is northeast')
assert(weather.compass(90) == 'E', '90 is east')
assert(weather.compass(135) == 'SE', '135 is southeast')
assert(weather.compass(180) == 'S', '180 is south')
assert(weather.compass(225) == 'SW', '225 is southwest')
assert(weather.compass(270) == 'W', '270 is west')
assert(weather.compass(315) == 'NW', '315 is northwest')
assert(weather.compass(nil) == nil, 'missing direction stays missing')
-- The arrow points where the wind is going, and OWM reports where it comes
-- from, so a north wind draws a downward arrow.
assert(weather.arrow(0) == '\u{2193}', 'a north wind blows southward')
assert(weather.arrow(180) == '\u{2191}', 'a south wind blows northward')
assert(weather.arrow(nil) == '', 'no direction draws nothing')
-- === Sun position =========================================================
-- t is the fraction of daylight elapsed, and the widget uses it to place the
-- dot along the arc. Sunrise 1000, sunset 2000, so midday is 1500.
assert(weather.sun_t(1000, 1000, 2000) == 0, 'at sunrise t is 0')
assert(weather.sun_t(1500, 1000, 2000) == 0.5, 'at midday t is half')
assert(weather.sun_t(2000, 1000, 2000) == 1, 'at sunset t is 1')
-- OWM reports today's sunrise and sunset, so between midnight and sunrise the
-- numerator is negative. The clamp is the whole handling; without it the dot
-- would be drawn off the left end of the arc.
assert(weather.sun_t(500, 1000, 2000) == 0, 'before dawn clamps to 0')
assert(weather.sun_t(9999, 1000, 2000) == 1, 'after dusk clamps to 1')
-- Degenerate input must not divide by zero.
assert(weather.sun_t(1500, 2000, 2000) == 0, 'zero-length day gives 0, not nan')
assert(weather.sun_t(nil, 1000, 2000) == nil, 'missing now gives nil')
assert(weather.sun_t(1500, nil, 2000) == nil, 'missing sunrise gives nil')
-- is_day drives both the icon face and the dot's colour: a dot parked at the
-- end of the arc must not read as a sun that is still up.
assert(weather.is_day(1500, 1000, 2000) == true, 'midday is day')
assert(weather.is_day(1000, 1000, 2000) == true, 'sunrise counts as day')
assert(weather.is_day(2000, 1000, 2000) == true, 'sunset counts as day')
assert(weather.is_day(500, 1000, 2000) == false, 'before dawn is night')
assert(weather.is_day(2500, 1000, 2000) == false, 'after dusk is night')
print('test_weather: all assertions passed')
|