-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsleep_test.go
More file actions
394 lines (349 loc) · 11.8 KB
/
sleep_test.go
File metadata and controls
394 lines (349 loc) · 11.8 KB
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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
package ditzy
import "testing"
func TestMrmlncSleepDetection(t *testing.T) {
// Real activity data from mrmlnc user
// Sleep should start after the active 0.0/0.5 buckets (14/13 events)
halfHourCounts := map[float64]int{
0.0: 14, // 03:00 Moscow
0.5: 13, // 03:30 Moscow
1.0: 2, // 04:00 Moscow
1.5: 2, // 04:30 Moscow
2.0: 1, // 05:00 Moscow
2.5: 0, // 05:30 Moscow (quiet)
3.0: 0, // 06:00 Moscow (quiet)
3.5: 0, // 06:30 Moscow (quiet)
4.0: 0, // 07:00 Moscow (quiet)
4.5: 0, // 07:30 Moscow (quiet)
5.0: 0, // 08:00 Moscow (quiet)
5.5: 1, // 08:30 Moscow
6.0: 1, // 09:00 Moscow
6.5: 2, // 09:30 Moscow (waking up)
7.0: 3, // 10:00 Moscow (awake)
7.5: 5, // 10:30 Moscow
8.0: 1, // 11:00 Moscow
8.5: 6, // 11:30 Moscow
9.0: 5, // 12:00 Moscow
9.5: 11, // 12:30 Moscow
10.0: 5, // 13:00 Moscow (lunch)
10.5: 7, // 13:30 Moscow
11.0: 3, // 14:00 Moscow
11.5: 8, // 14:30 Moscow
12.0: 13, // 15:00 Moscow
12.5: 11, // 15:30 Moscow
13.0: 8, // 16:00 Moscow
13.5: 15, // 16:30 Moscow (peak)
14.0: 7, // 17:00 Moscow
14.5: 3, // 17:30 Moscow
15.0: 2, // 18:00 Moscow
15.5: 1, // 18:30 Moscow
16.0: 6, // 19:00 Moscow
16.5: 1, // 19:30 Moscow
17.0: 4, // 20:00 Moscow
17.5: 0, // 20:30 Moscow (quiet)
18.0: 0, // 21:00 Moscow (quiet)
18.5: 0, // 21:30 Moscow (quiet)
19.0: 1, // 22:00 Moscow
19.5: 12, // 22:30 Moscow (active)
20.0: 12, // 23:00 Moscow (active)
20.5: 10, // 23:30 Moscow (active)
21.0: 14, // 00:00 Moscow (active)
21.5: 13, // 00:30 Moscow (active)
22.0: 2, // 01:00 Moscow
22.5: 2, // 01:30 Moscow
23.0: 1, // 02:00 Moscow
23.5: 0, // 02:30 Moscow (QUIET)
}
// Detect sleep periods with Moscow timezone offset (+3)
sleepBuckets := detectSleepBuckets(halfHourCounts, 3)
t.Logf("Detected sleep buckets: %v", sleepBuckets)
// Check that sleep starts after the active 0.0 (14 events) and 0.5 (13 events) buckets
if len(sleepBuckets) > 0 && sleepBuckets[0] < 1.0 {
t.Errorf("Sleep should start at or after 1.0 (04:00 Moscow), got %.1f", sleepBuckets[0])
}
if len(sleepBuckets) == 0 {
t.Fatal("No sleep periods detected")
}
// For wraparound sleep, we should have evening buckets (22-24) or early morning buckets (0-6)
hasEveningStart := false
for _, b := range sleepBuckets {
if b >= 22.0 && b < 24.0 {
hasEveningStart = true
break
}
}
if !hasEveningStart && sleepBuckets[0] > 6.0 {
t.Errorf("Sleep should include evening hours or start in early morning, got first bucket: %v", sleepBuckets[0])
}
}
func TestSleepDetectionWithQuietEvening(t *testing.T) {
halfHourCounts := map[float64]int{
22.0: 2, // minimal activity
22.5: 1, // minimal activity
23.0: 0, // quiet (bedtime)
23.5: 0, // quiet (asleep)
0.0: 1, // minimal (bathroom break?)
0.5: 1, // minimal
1.0: 0, // quiet
1.5: 0, // quiet
2.0: 0, // quiet
2.5: 0, // quiet
3.0: 0, // quiet
3.5: 0, // quiet
4.0: 0, // quiet
4.5: 0, // quiet
5.0: 0, // quiet
5.5: 1, // minimal activity
6.0: 2, // waking up
6.5: 3, // awake
7.0: 5, // active
8.0: 8,
9.0: 10,
10.0: 12,
11.0: 8,
12.0: 4, // lunch dip
13.0: 6,
14.0: 10,
15.0: 12,
16.0: 8,
17.0: 6,
18.0: 4,
19.0: 3,
20.0: 2,
21.0: 1,
21.5: 1,
}
sleepBuckets := detectSleepBuckets(halfHourCounts, 0)
// Should include evening buckets 23.0 and 23.5
found23_0 := false
found23_5 := false
for _, bucket := range sleepBuckets {
if bucket == 23.0 {
found23_0 = true
}
if bucket == 23.5 {
found23_5 = true
}
}
if !found23_0 || !found23_5 {
t.Errorf("Expected sleep to include evening buckets 23.0 and 23.5, found 23.0=%v, 23.5=%v", found23_0, found23_5)
t.Errorf("Detected sleep buckets: %v", sleepBuckets)
}
}
// TestRebelopsioSleepDetection tests the specific case where rebelopsio should have
// 21:30-06:30 rest hours (or close to it) and NOT include evening activity as sleep
func TestRebelopsioSleepDetection(t *testing.T) {
// rebelopsio's actual half-hourly activity pattern (UTC times for America/New_York UTC-4)
halfHourCounts := map[float64]int{
// Early morning activity (UTC times = local-4)
0.0: 4, // 00:00 UTC = 20:00 local - evening activity
0.5: 0, // 00:30 UTC = 20:30 local
1.0: 3, // 01:00 UTC = 21:00 local - evening activity
1.5: 0, // 01:30 UTC = 21:30 local - start of rest
// Rest period (should be detected as sleep)
2.0: 0, // 02:00 UTC = 22:00 local
2.5: 0, // 02:30 UTC = 22:30 local
3.0: 0, // 03:00 UTC = 23:00 local
3.5: 0, // 03:30 UTC = 23:30 local
4.0: 0, // 04:00 UTC = 00:00 local
4.5: 0, // 04:30 UTC = 00:30 local
5.0: 0, // 05:00 UTC = 01:00 local
5.5: 0, // 05:30 UTC = 01:30 local
6.0: 0, // 06:00 UTC = 02:00 local
6.5: 0, // 06:30 UTC = 02:30 local
7.0: 0, // 07:00 UTC = 03:00 local
7.5: 0, // 07:30 UTC = 03:30 local
8.0: 0, // 08:00 UTC = 04:00 local
8.5: 0, // 08:30 UTC = 04:30 local
9.0: 0, // 09:00 UTC = 05:00 local
9.5: 0, // 09:30 UTC = 05:30 local
10.0: 1, // 10:00 UTC = 06:00 local - end of rest, minimal activity
// Morning activity resumes (should NOT be sleep)
10.5: 5, // 10:30 UTC = 06:30 local - work starts
11.0: 2, // 11:00 UTC = 07:00 local
11.5: 0, // 11:30 UTC = 07:30 local
12.0: 6, // 12:00 UTC = 08:00 local
12.5: 2, // 12:30 UTC = 08:30 local
13.0: 1, // 13:00 UTC = 09:00 local
13.5: 1, // 13:30 UTC = 09:30 local
// Work day continues
14.0: 6, // 14:00 UTC = 10:00 local
15.0: 7, // 15:00 UTC = 11:00 local
16.0: 10, // 16:00 UTC = 12:00 local - lunch time
17.0: 8, // 17:00 UTC = 13:00 local
18.0: 13, // 18:00 UTC = 14:00 local
19.0: 7, // 19:00 UTC = 15:00 local
20.0: 21, // 20:00 UTC = 16:00 local - peak activity
21.0: 4, // 21:00 UTC = 17:00 local
22.0: 3, // 22:00 UTC = 18:00 local
// Evening activity (should NOT be considered sleep despite low counts)
23.0: 2, // 23:00 UTC = 19:00 local - light evening activity
23.5: 2, // 23:30 UTC = 19:30 local - light evening activity
}
sleepBuckets := detectSleepBuckets(halfHourCounts, -4)
if len(sleepBuckets) == 0 {
t.Fatal("No sleep buckets detected, but should have detected rest period from ~22:00-06:00 local")
}
t.Logf("Detected sleep buckets: %v", sleepBuckets)
// Create a set for easier checking
sleepSet := make(map[float64]bool)
for _, bucket := range sleepBuckets {
sleepSet[bucket] = true
}
// Morning work buckets after 12.0 should definitely NOT be included
morningWorkBuckets := []float64{12.0, 12.5} // 8:00-8:30 local
for _, bucket := range morningWorkBuckets {
if sleepSet[bucket] {
t.Errorf("Sleep period should NOT include bucket %.1f (morning work activity)", bucket)
}
}
// Verify we have a reasonable sleep duration (at least 6 hours = 12 buckets)
if len(sleepBuckets) < 12 {
t.Errorf("Expected at least 12 sleep buckets for 6+ hour rest period, got %d", len(sleepBuckets))
}
if len(sleepBuckets) > 48 {
t.Errorf("Sleep buckets should not exceed 24 hours (48 buckets), got %d", len(sleepBuckets))
}
}
func TestBinacsUTC8SleepDetection(t *testing.T) {
// Binacs actual activity pattern - VERY sparse data (only 14 events)
halfHourCounts := map[float64]int{
0.0: 0, 0.5: 0,
1.0: 0, 1.5: 0,
2.0: 0, 2.5: 0,
3.0: 0, 3.5: 0,
4.0: 0, 4.5: 0,
5.0: 0, 5.5: 0,
6.0: 0, 6.5: 0,
7.0: 3, 7.5: 0, // 15:00 local (UTC+8) - afternoon activity
8.0: 0, 8.5: 0,
9.0: 3, 9.5: 0, // 17:00 local - late afternoon
10.0: 0, 10.5: 1, // 18:30 local - evening
11.0: 1, 11.5: 0, // 19:00 local - evening
12.0: 0, 12.5: 3, // 20:30 local - evening
13.0: 0, 13.5: 0,
14.0: 0, 14.5: 0,
15.0: 1, 15.5: 0, // 23:00 local - late evening
16.0: 1, 16.5: 0, // 00:00 local - should be sleep time!
17.0: 0, 17.5: 0,
18.0: 0, 18.5: 1, // 02:30 local - should be sleep!
19.0: 0, 19.5: 0,
20.0: 0, 20.5: 0,
21.0: 0, 21.5: 0,
22.0: 0, 22.5: 0,
23.0: 0, 23.5: 0,
}
tests := []struct {
name string
description string
offset float64
}{
{
name: "UTC+8 (China) - sleep should be around midnight-8am local",
offset: 8,
description: "Sleep for UTC+8 should detect late night/early morning quiet period or nothing",
},
{
name: "UTC+0 (Western bias) - would incorrectly detect morning as sleep",
offset: 0,
description: "UTC+0 bias incorrectly identifies Asian morning hours as sleep",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sleepBuckets := detectSleepBuckets(halfHourCounts, tt.offset)
t.Logf("Detected sleep buckets for offset %+.0f: %v", tt.offset, sleepBuckets)
t.Logf("Description: %s", tt.description)
if len(sleepBuckets) == 0 {
if tt.offset == 8.0 {
t.Logf("No sleep detected for UTC+8 - acceptable for sparse data with nighttime activity")
return
}
t.Errorf("No sleep detected for offset %+.0f", tt.offset)
return
}
minBucket := sleepBuckets[0]
maxBucket := sleepBuckets[len(sleepBuckets)-1]
if tt.offset == 8 {
if minBucket < 14.0 && minBucket > 2.0 {
t.Errorf("UTC+8: Sleep starts at %.1f UTC (%.1f local), which is too early for nighttime",
minBucket, minBucket+8)
}
morningQuietDetected := false
for _, bucket := range sleepBuckets {
if bucket >= 1.0 && bucket <= 10.0 {
morningQuietDetected = true
break
}
}
if morningQuietDetected {
t.Errorf("UTC+8: Incorrectly detected morning quiet hours (1:00-10:00 UTC) as sleep")
}
}
t.Logf("Sleep period: %.1f-%.1f UTC (%.1f-%.1f local)",
minBucket, maxBucket,
normHour(minBucket+float64(tt.offset)),
normHour(maxBucket+float64(tt.offset)))
})
}
}
// TestXnoxSleepDetection tests the specific case where xnox should have 05:00-09:30 rest hours
func TestXnoxSleepDetection(t *testing.T) {
// xnox's actual half-hourly activity pattern (UTC times converted to local UTC+1)
halfHourCounts := map[float64]int{
// Late night activity (UTC times, which is local UTC+1 times - 1)
0.0: 23, // 01:00 local - peak activity
0.5: 25, // 01:30 local
1.0: 33, // 02:00 local - morning peak
1.5: 4, // 02:30 local
2.0: 11, // 03:00 local
2.5: 12, // 03:30 local
3.0: 6, // 04:00 local
3.5: 5, // 04:30 local
4.0: 6, // 05:00 local
4.5: 4, // 05:30 local
// Rest period should start here
5.0: 0, // 06:00 local (05:00 rest start)
5.5: 2, // 06:30 local (05:30)
6.0: 1, // 07:00 local (06:00)
6.5: 1, // 07:30 local (06:30)
7.0: 0, // 08:00 local (07:00)
7.5: 0, // 08:30 local (07:30)
8.0: 0, // 09:00 local (08:00)
8.5: 1, // 09:30 local (08:30)
9.0: 2, // 10:00 local (09:00)
// Rest period should end here at 09:30 local
9.5: 6, // 10:30 local (09:30) - activity resumes
10.0: 9, // 11:00 local
10.5: 3, // 11:30 local
11.0: 5, // 12:00 local
11.5: 28, // 12:30 local - pre-lunch activity
12.0: 11, // 13:00 local - lunch dip
12.5: 21, // 13:30 local
}
sleepBuckets := detectSleepBuckets(halfHourCounts, 1)
expectedBuckets := []float64{5.0, 5.5, 6.0, 6.5, 7.0, 7.5, 8.0, 8.5, 9.0}
if len(sleepBuckets) == 0 {
t.Fatal("No sleep buckets detected, but should have detected 05:00-09:30 rest period")
}
t.Logf("Detected sleep buckets: %v", sleepBuckets)
t.Logf("Expected sleep buckets: %v", expectedBuckets)
if len(sleepBuckets) < 8 {
t.Errorf("Expected at least 8 sleep buckets for 4+ hour rest period, got %d", len(sleepBuckets))
}
sleepSet := make(map[float64]bool)
for _, bucket := range sleepBuckets {
sleepSet[bucket] = true
}
criticalBuckets := []float64{6.0, 7.0, 7.5, 8.0}
for _, bucket := range criticalBuckets {
if !sleepSet[bucket] {
t.Errorf("Sleep period should include bucket %.1f (critical quiet time)", bucket)
}
}
if !sleepSet[5.0] {
t.Errorf("Sleep period should include bucket 5.0 (start of rest at 05:00 local)")
}
if !sleepSet[9.0] {
t.Errorf("Sleep period should include bucket 9.0 (end of rest at 09:00 local)")
}
}