добавлен словарь категориальных значений HAE → коды HealthKit
- фазы сна, контекст пульса и имена тренировок попадают в реестр `category_value` (миграция 00010): строка хранится дословно, выведенный код лежит рядом отдельной записью, а не полем внутри точки - словарь и синонимы кодов живут в бинаре (`internal/healthkit`); локаль из `Accept-Language` сужает поиск, но в ключ реестра не входит — заголовков в сыром архиве нет - наблюдение входит в отпечаток витрины, выведенный код — нет: он производная от словаря, а не от журнала
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user