добавлен словарь категориальных значений HAE → коды HealthKit
- фазы сна, контекст пульса и имена тренировок попадают в реестр `category_value` (миграция 00010): строка хранится дословно, выведенный код лежит рядом отдельной записью, а не полем внутри точки - словарь и синонимы кодов живут в бинаре (`internal/healthkit`); локаль из `Accept-Language` сужает поиск, но в ключ реестра не входит — заголовков в сыром архиве нет - наблюдение входит в отпечаток витрины, выведенный код — нет: он производная от словаря, а не от журнала
This commit is contained in:
@@ -179,9 +179,14 @@ type report struct {
|
||||
// единица, которой нет в счётчиках, делает расхождение безадресным.
|
||||
sourceWorkouts int64
|
||||
sourceRecords int64
|
||||
sourceBefore int64
|
||||
sourceAfter int64
|
||||
sourceMissing bool
|
||||
// sourceCategories — то же «было» для реестра категориальных значений.
|
||||
// Перечень единиц хранения закрытый, и он пополняется ТЕМ ЖЕ изменением,
|
||||
// которое заводит единицу: не внесённая сюда, она молчит ровно там, где
|
||||
// расхождение впервые становится заметным.
|
||||
sourceCategories int64
|
||||
sourceBefore int64
|
||||
sourceAfter int64
|
||||
sourceMissing bool
|
||||
}
|
||||
|
||||
// rebuild собирает витрину в промежуточный файл и переименовывает его в файл
|
||||
@@ -237,6 +242,9 @@ func rebuild(ctx context.Context, cfg *config.Config, t target, log *slog.Logger
|
||||
if rep.sourceRecords, err = src.CountRecords(ctx); err != nil {
|
||||
return canceledOr(rep, err, stopped)
|
||||
}
|
||||
if rep.sourceCategories, err = src.CountCategoryValues(ctx); err != nil {
|
||||
return canceledOr(rep, err, stopped)
|
||||
}
|
||||
}
|
||||
|
||||
removeDB(t.partial)
|
||||
|
||||
@@ -66,6 +66,7 @@ func writeReport(w io.Writer, r report) {
|
||||
p(" объектов: %d", r.replay.Buckets)
|
||||
p(" тренировок: %d", r.replay.Workouts)
|
||||
p(" записей: %d", r.replay.Records)
|
||||
p(" строк реестра категориальных значений: %d", r.replay.Categories)
|
||||
p("")
|
||||
p("рабочей базы %s нет — сверять не с чем, а заголовки доставок", r.dbPath)
|
||||
p("не восстанавливаются: в архиве их нет.")
|
||||
@@ -76,6 +77,8 @@ func writeReport(w io.Writer, r report) {
|
||||
p(" объектов: было %d, стало %d", r.sourceBuckets, r.replay.Buckets)
|
||||
p(" тренировок: было %d, стало %d", r.sourceWorkouts, r.replay.Workouts)
|
||||
p(" записей: было %d, стало %d", r.sourceRecords, r.replay.Records)
|
||||
p(" строк реестра категориальных значений: было %d, стало %d",
|
||||
r.sourceCategories, r.replay.Categories)
|
||||
p("")
|
||||
p(" отпечаток рабочей: %s", r.sourcePrint)
|
||||
p(" отпечаток пересобранной: %s", r.replay.Fingerprint)
|
||||
@@ -89,6 +92,26 @@ func writeReport(w io.Writer, r report) {
|
||||
p(" ожидаемые причины: исправленный разбор; покрытая разбором новая")
|
||||
p(" секция (её единиц хранения в рабочей базе нет по построению);")
|
||||
p(" признак sealed не переносится (правила его выставления ещё нет)")
|
||||
if r.sourceCategories < r.replay.Categories {
|
||||
// Класс назван отдельно от факта расхождения: реестр появился
|
||||
// вместе с бинарём, и у витрины, свёрнутой прежним, его нет по
|
||||
// построению. Не назвав это, отчёт приучает человека
|
||||
// игнорировать расхождение — то есть обесценивает оракул ровно
|
||||
// там, где по нему принимается необратимое решение.
|
||||
//
|
||||
// Условие — НЕПОЛНОТА, а не пустота. Между выкаткой и прогоном
|
||||
// проходят дни: воркер успевает набрать частые значения (фазы
|
||||
// сна, контекст пульса) и не успевает редкие — имя тренировки,
|
||||
// которая с тех пор не повторялась. Проверка «в рабочей базе
|
||||
// реестра нет вовсе» такое состояние не ловила бы, и человек
|
||||
// получил бы безадресное «разошлись» при совпавших числах
|
||||
// объектов, тренировок и записей.
|
||||
p(" РЕЕСТР НЕПОЛОН: строк категориальных значений в рабочей базе %d,",
|
||||
r.sourceCategories)
|
||||
p(" в пересобранной %d — реестр наполняется по мере свёртки, а целиком",
|
||||
r.replay.Categories)
|
||||
p(" его даёт только пересборка. Расхождение объясняется этим и лечится ею же")
|
||||
}
|
||||
if partialJournal {
|
||||
p(" ВНИМАНИЕ: часть журнала не прочитана — расхождение может")
|
||||
p(" объясняться этим, а не разбором")
|
||||
|
||||
@@ -337,3 +337,109 @@ func TestОтчётВсегдаНазываетУдержанныеВерсии(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Реестр категориальных значений — четвёртая единица хранения витрины, и у
|
||||
// витрины, свёрнутой прежним бинарём, его нет по построению. Расхождение
|
||||
// отпечатков по нему одному законно, и отчёт обязан назвать это классом, а не
|
||||
// оставить человека с безадресным «не совпало»: числа объектов, тренировок и
|
||||
// записей при этом не меняются вовсе, а решение о подмене базы необратимо.
|
||||
func TestОтчётНазываетПоявившийсяРеестр(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var buf bytes.Buffer
|
||||
writeReport(&buf, report{
|
||||
replay: replay.Report{
|
||||
Bodies: 116,
|
||||
Outcome: replay.Outcome{Folded: 116},
|
||||
Buckets: 2049, Workouts: 2, Records: 2, Categories: 11,
|
||||
Fingerprint: "aaaa",
|
||||
},
|
||||
target: "/data/healthlog.db.rebuild",
|
||||
dbPath: "/data/healthlog.db",
|
||||
sourcePrint: "bbbb",
|
||||
sourceBuckets: 2049,
|
||||
sourceWorkouts: 2,
|
||||
sourceRecords: 2,
|
||||
sourceCategories: 0,
|
||||
sourceBefore: 116,
|
||||
sourceAfter: 116,
|
||||
})
|
||||
out := buf.String()
|
||||
|
||||
for _, want := range []string{
|
||||
"строк реестра категориальных значений: было 0, стало 11",
|
||||
"РЕЕСТР НЕПОЛОН",
|
||||
"лечится ею же",
|
||||
} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("отчёт не содержит %q:\n%s", want, out)
|
||||
}
|
||||
}
|
||||
|
||||
// Сами строки реестра — данные о здоровье наравне со значением точки:
|
||||
// отчёт отвечает счётом, а не перечислением.
|
||||
for _, forbidden := range []string{"Во сне", "Сидячий образ жизни", "HKCategoryValue"} {
|
||||
if strings.Contains(out, forbidden) {
|
||||
t.Errorf("отчёт содержит наблюдённую строку %q", forbidden)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Реестр рабочей витрины непуст, но неполон — штатное состояние через сутки
|
||||
// после выкатки: частые значения воркер набрал, редкое имя тренировки с тех пор
|
||||
// не повторялось. Класс обязан называться и здесь, иначе человек получит
|
||||
// безадресное «разошлись» при совпавших числах объектов, тренировок и записей —
|
||||
// и научится игнорировать строку, по которой принимает необратимое решение.
|
||||
func TestОтчётНазываетНеполныйРеестр(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var buf bytes.Buffer
|
||||
writeReport(&buf, report{
|
||||
replay: replay.Report{
|
||||
Bodies: 116,
|
||||
Outcome: replay.Outcome{Folded: 116},
|
||||
Buckets: 2049, Workouts: 2, Records: 2, Categories: 11,
|
||||
Fingerprint: "aaaa",
|
||||
},
|
||||
target: "/data/healthlog.db.rebuild",
|
||||
dbPath: "/data/healthlog.db",
|
||||
sourcePrint: "bbbb",
|
||||
sourceBuckets: 2049,
|
||||
sourceWorkouts: 2,
|
||||
sourceRecords: 2,
|
||||
sourceCategories: 5,
|
||||
sourceBefore: 116,
|
||||
sourceAfter: 116,
|
||||
})
|
||||
out := buf.String()
|
||||
for _, want := range []string{"РЕЕСТР НЕПОЛОН", "в рабочей базе 5", "в пересобранной 11"} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("отчёт не содержит %q:\n%s", want, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Совпавший реестр отдельным классом не объявляется: иначе строка звучала бы
|
||||
// при каждом прогоне и перестала бы что-либо значить.
|
||||
func TestОтчётНеОбъявляетРеестрПоявившимсяБезПричины(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var buf bytes.Buffer
|
||||
writeReport(&buf, report{
|
||||
replay: replay.Report{
|
||||
Bodies: 10,
|
||||
Outcome: replay.Outcome{Folded: 10},
|
||||
Buckets: 5, Categories: 11, Fingerprint: "aaaa",
|
||||
},
|
||||
target: "/data/healthlog.db.rebuild",
|
||||
dbPath: "/data/healthlog.db",
|
||||
sourcePrint: "bbbb",
|
||||
sourceBuckets: 4,
|
||||
sourceCategories: 11,
|
||||
sourceBefore: 10,
|
||||
sourceAfter: 10,
|
||||
})
|
||||
if out := buf.String(); strings.Contains(out, "РЕЕСТР НЕПОЛОН") {
|
||||
t.Errorf("класс объявлен при совпавшем реестре рабочей витрины:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user