Files
healthlog/internal/healthkit/healthkit_test.go
T
av 1b649ba3d5 добавлен словарь категориальных значений HAE → коды HealthKit
- фазы сна, контекст пульса и имена тренировок попадают в реестр
  `category_value` (миграция 00010): строка хранится дословно, выведенный код
  лежит рядом отдельной записью, а не полем внутри точки
- словарь и синонимы кодов живут в бинаре (`internal/healthkit`); локаль из
  `Accept-Language` сужает поиск, но в ключ реестра не входит — заголовков в
  сыром архиве нет
- наблюдение входит в отпечаток витрины, выведенный код — нет: он производная
  от словаря, а не от журнала
2026-08-04 07:32:19 +03:00

139 lines
5.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package healthkit_test
import (
"strings"
"testing"
"git.vakhrushev.me/av/healthlog/internal/healthkit"
)
// Обе формы имени, которые Apple дала одному значению, обязаны сойтись в один
// код: иначе история расколется вторично, уже на «стабильной» стороне
// (находка 43).
func TestCanonicalОбеФормыСходятся(t *testing.T) {
t.Parallel()
const (
old = "HKCategoryValueSleepAnalysisAsleep"
now = "HKCategoryValueSleepAnalysisAsleepUnspecified"
)
if got := healthkit.Canonical(old); got != now {
t.Errorf("устаревшее имя %q дало %q, ожидалось %q", old, got, now)
}
if got := healthkit.Canonical(now); got != now {
t.Errorf("нынешнее имя %q дало %q, ожидалось %q", now, got, now)
}
}
func TestCanonicalКодБезСинонимаОстаётсяСобой(t *testing.T) {
t.Parallel()
const code = "HKCategoryValueSleepAnalysisAsleepREM"
if got := healthkit.Canonical(code); got != code {
t.Errorf("код без синонима стал %q", got)
}
if got := healthkit.Canonical(""); got != "" {
t.Errorf("пустой код стал %q", got)
}
}
// Плоскость таблицы синонимов — то, чем оправдано отсутствие обхода цепочек в
// рантайме. Проверяется по таблице целиком, а не на примере: правило держится
// на всей таблице, и первая же добавленная запись может его нарушить.
//
// Обходим через Canonical, а не через саму карту: наружу она не отдаётся, а
// «значение не является ключом» проверяемо снаружи — канонизация значения
// обязана быть неподвижной точкой.
func TestSynonymsТаблицаПлоская(t *testing.T) {
t.Parallel()
// Значения таблицы наблюдаемы через Code: словарь уже канонизирован, а
// коды, которые он выдаёт, обязаны быть неподвижными точками.
for _, value := range []string{"Основная", "Бодрствование", "БДГ", "Глубокий", "В кровати", "Во сне"} {
code := healthkit.Code("ru", value)
if code == "" {
t.Fatalf("измеренная строка %q кода не дала", value)
}
if again := healthkit.Canonical(code); again != code {
t.Errorf("код %q не неподвижная точка: канонизация дала %q — таблица синонимов не плоская", code, again)
}
}
// То же для известного алиаса: его канонизация обязана быть неподвижной с
// одного шага.
once := healthkit.Canonical("HKCategoryValueSleepAnalysisAsleep")
if twice := healthkit.Canonical(once); twice != once {
t.Errorf("алиас разрешился не за один шаг: %q → %q", once, twice)
}
}
func TestCodeФазыСна(t *testing.T) {
t.Parallel()
cases := []struct {
locale string
value string
want string
}{
{"ru", "Основная", "HKCategoryValueSleepAnalysisAsleepCore"},
{"ru", "Бодрствование", "HKCategoryValueSleepAnalysisAwake"},
{"ru", "БДГ", "HKCategoryValueSleepAnalysisAsleepREM"},
{"ru", "Глубокий", "HKCategoryValueSleepAnalysisAsleepDeep"},
{"ru", "В кровати", "HKCategoryValueSleepAnalysisInBed"},
{"ru", "Во сне", "HKCategoryValueSleepAnalysisAsleepUnspecified"},
// Локали нет — код всё равно выводится вторым разрядом: заголовков в
// сыром архиве не лежит, и усыновлённое тело обязано дать то же
// состояние.
{"", "Во сне", "HKCategoryValueSleepAnalysisAsleepUnspecified"},
// Незнакомая локаль знакомой строки код не отменяет.
{"en", "Во сне", "HKCategoryValueSleepAnalysisAsleepUnspecified"},
// Догадок нет.
{"ru", "Полудрёма", ""},
{"ru", "", ""},
{"", "Сидячий образ жизни", ""},
}
for _, c := range cases {
t.Run(c.locale+"/"+c.value, func(t *testing.T) {
t.Parallel()
if got := healthkit.Code(c.locale, c.value); got != c.want {
t.Errorf("Code(%q, %q) = %q, ожидалось %q", c.locale, c.value, got, c.want)
}
})
}
}
func TestLocaleНормализация(t *testing.T) {
t.Parallel()
cases := []struct {
name string
header string
want string
}{
{"измеренное значение потока", "ru", "ru"},
{"верхний регистр", "RU", "ru"},
{"подтег", "ru-RU", "ru"},
{"список с весами и регистром", "RU-ru,ru;q=0.9,en;q=0.8", "ru"},
{"пробелы вокруг", " ru ", "ru"},
{"вес без списка", "ru;q=1", "ru"},
{"звёздочка тегом не является", "*", ""},
{"пустой заголовок", "", ""},
{"мусор", "!!!", ""},
{"однобуквенный тег", "r", ""},
{"непомерно длинный тег", strings.Repeat("x", 64), ""},
{"сто тегов", strings.Repeat("en,", 100) + "ru", "en"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
t.Parallel()
if got := healthkit.Locale(c.header); got != c.want {
t.Errorf("Locale(%q) = %q, ожидалось %q", c.header, got, c.want)
}
})
}
}