feat: разбор и хранение тренировок и состояния разума
- секции `workouts` и `stateOfMind` покрыты разбором: тренировка лежит одной строкой вместе с маршрутом и внутренними рядами, запись — по ключу `род + id`; миграция 00007 заводит обе таблицы и возвращает в очередь `partial`-доставки с этими ключами - сущность заменяется целиком, но условно: приехавшая побеждает, если не теряет содержания сохранённой (множество ключей и длины верхнеуровневых массивов), а при равном содержании выигрывает версия из более поздней доставки ЖУРНАЛА — «побеждает приехавшая» было бы функцией порядка свёртки, и живая витрина расходилась бы с пересборкой молча - отпечаток витрины покрывает тренировки и записи и снимается одним снимком базы; отчёт `reindex` считает «было и стало» по каждой единице хранения
This commit is contained in:
@@ -51,7 +51,7 @@ func point(t *testing.T, metric, layer, start, end, raw string) store.IncomingPo
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergePointsКладётЧасОднимОбъектом(t *testing.T) {
|
||||
func TestMergeКладётЧасОднимОбъектом(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
@@ -63,7 +63,7 @@ func TestMergePointsКладётЧасОднимОбъектом(t *testing.T) {
|
||||
point(t, "step_count", "minute", "2025-06-05T11:00:00Z", "2025-06-05T11:00:00Z", `{"qty":3}`),
|
||||
}
|
||||
|
||||
stats, err := st.MergePoints(ctx, in, "delivery-1")
|
||||
stats, err := st.Merge(ctx, store.Incoming{Points: in}, store.DeliveryRef{ID: "delivery-1"})
|
||||
if err != nil {
|
||||
t.Fatalf("слияние: %v", err)
|
||||
}
|
||||
@@ -91,7 +91,7 @@ func TestMergePointsКладётЧасОднимОбъектом(t *testing.T) {
|
||||
|
||||
// Дозапись в существующий час: объект перечитывается, точки сливаются, ранее
|
||||
// сохранённые остаются. Точки из объекта не удаляются никогда.
|
||||
func TestMergePointsДозаписьНеТеряетСохранённое(t *testing.T) {
|
||||
func TestMergeДозаписьНеТеряетСохранённое(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
@@ -104,10 +104,10 @@ func TestMergePointsДозаписьНеТеряетСохранённое(t *te
|
||||
point(t, "step_count", "minute", "2025-06-05T10:30:00Z", "2025-06-05T10:30:00Z", `{"qty":2}`),
|
||||
}
|
||||
|
||||
if _, err := st.MergePoints(ctx, first, "d1"); err != nil {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: first}, store.DeliveryRef{ID: "d1"}); err != nil {
|
||||
t.Fatalf("первое слияние: %v", err)
|
||||
}
|
||||
if _, err := st.MergePoints(ctx, second, "d2"); err != nil {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: second}, store.DeliveryRef{ID: "d2"}); err != nil {
|
||||
t.Fatalf("второе слияние: %v", err)
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ func TestMergePointsДозаписьНеТеряетСохранённое(t *te
|
||||
|
||||
// Хеш — детектор изменений: повтор того же часа не пишет в базу. Именно это
|
||||
// делает широкие проходы синхронизации дешёвыми.
|
||||
func TestMergePointsПовторНеПишет(t *testing.T) {
|
||||
func TestMergeПовторНеПишет(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
@@ -136,7 +136,7 @@ func TestMergePointsПовторНеПишет(t *testing.T) {
|
||||
point(t, "step_count", "minute", "2025-06-05T10:01:00Z", "2025-06-05T10:01:00Z", `{"qty":2,"source":"Device A"}`),
|
||||
}
|
||||
|
||||
if _, err := st.MergePoints(ctx, in, "d1"); err != nil {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: in}, store.DeliveryRef{ID: "d1"}); err != nil {
|
||||
t.Fatalf("первое слияние: %v", err)
|
||||
}
|
||||
before, err := st.Bucket(ctx, "step_count", "minute", ts(t, "2025-06-05T10:00:00Z"))
|
||||
@@ -144,7 +144,7 @@ func TestMergePointsПовторНеПишет(t *testing.T) {
|
||||
t.Fatalf("чтение: %v", err)
|
||||
}
|
||||
|
||||
stats, err := st.MergePoints(ctx, in, "d2")
|
||||
stats, err := st.Merge(ctx, store.Incoming{Points: in}, store.DeliveryRef{ID: "d2"})
|
||||
if err != nil {
|
||||
t.Fatalf("повторное слияние: %v", err)
|
||||
}
|
||||
@@ -172,7 +172,7 @@ func TestMergePointsПовторНеПишет(t *testing.T) {
|
||||
|
||||
// Порядок точек внутри доставки нестабилен (находка 2), и идентичность не
|
||||
// имеет права от него зависеть.
|
||||
func TestMergePointsИдемпотентенКПорядкуТочек(t *testing.T) {
|
||||
func TestMergeИдемпотентенКПорядкуТочек(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
@@ -185,7 +185,7 @@ func TestMergePointsИдемпотентенКПорядкуТочек(t *testin
|
||||
}
|
||||
reversed := []store.IncomingPoint{in[2], in[1], in[0]}
|
||||
|
||||
if _, err := st.MergePoints(ctx, in, "d1"); err != nil {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: in}, store.DeliveryRef{ID: "d1"}); err != nil {
|
||||
t.Fatalf("первое слияние: %v", err)
|
||||
}
|
||||
first, err := st.Bucket(ctx, "step_count", "minute", ts(t, "2025-06-05T10:00:00Z"))
|
||||
@@ -193,7 +193,7 @@ func TestMergePointsИдемпотентенКПорядкуТочек(t *testin
|
||||
t.Fatalf("чтение: %v", err)
|
||||
}
|
||||
|
||||
stats, err := st.MergePoints(ctx, reversed, "d2")
|
||||
stats, err := st.Merge(ctx, store.Incoming{Points: reversed}, store.DeliveryRef{ID: "d2"})
|
||||
if err != nil {
|
||||
t.Fatalf("слияние в обратном порядке: %v", err)
|
||||
}
|
||||
@@ -213,7 +213,7 @@ func TestMergePointsИдемпотентенКПорядкуТочек(t *testin
|
||||
// Ключ по метке схлопнул бы эти три записи в одну. Живьём такое встречается в
|
||||
// 22 доставках из 94 (находка 47), причём внутри одной доставки — там тай-брейк
|
||||
// по времени приёма неприменим в принципе.
|
||||
func TestMergePointsЗаписиСОднойМеткойНеСхлопываются(t *testing.T) {
|
||||
func TestMergeЗаписиСОднойМеткойНеСхлопываются(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
@@ -225,7 +225,7 @@ func TestMergePointsЗаписиСОднойМеткойНеСхлопывают
|
||||
point(t, "sleep_analysis", "minute", "2025-06-05T19:04:00Z", "2025-06-06T04:51:00Z", `{"qty":9.78,"value":"В кровати"}`),
|
||||
}
|
||||
|
||||
if _, err := st.MergePoints(ctx, in, "d1"); err != nil {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: in}, store.DeliveryRef{ID: "d1"}); err != nil {
|
||||
t.Fatalf("слияние: %v", err)
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ func TestMergePointsЗаписиСОднойМеткойНеСхлопывают
|
||||
|
||||
// Повтор той же тройки не задваивает: координата включает интервал, и он
|
||||
// совпадает.
|
||||
stats, err := st.MergePoints(ctx, in, "d2")
|
||||
stats, err := st.Merge(ctx, store.Incoming{Points: in}, store.DeliveryRef{ID: "d2"})
|
||||
if err != nil {
|
||||
t.Fatalf("повтор: %v", err)
|
||||
}
|
||||
@@ -250,7 +250,7 @@ func TestMergePointsЗаписиСОднойМеткойНеСхлопывают
|
||||
|
||||
// Эпизод, пересекающий границу часа, ложится в объект по НАЧАЛУ: любой другой
|
||||
// выбор сделал бы принадлежность объекту зависящей от длительности.
|
||||
func TestMergePointsЧасПоНачалуИнтервала(t *testing.T) {
|
||||
func TestMergeЧасПоНачалуИнтервала(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
@@ -259,7 +259,7 @@ func TestMergePointsЧасПоНачалуИнтервала(t *testing.T) {
|
||||
in := []store.IncomingPoint{
|
||||
point(t, "sleep_analysis", "minute", "2025-06-05T19:04:00Z", "2025-06-06T04:51:00Z", `{"qty":9.78}`),
|
||||
}
|
||||
if _, err := st.MergePoints(ctx, in, "d1"); err != nil {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: in}, store.DeliveryRef{ID: "d1"}); err != nil {
|
||||
t.Fatalf("слияние: %v", err)
|
||||
}
|
||||
|
||||
@@ -274,7 +274,7 @@ func TestMergePointsЧасПоНачалуИнтервала(t *testing.T) {
|
||||
// Правило разрешения столкновений: выигрывает более полная точка, а не
|
||||
// последняя пришедшая. Иначе бедная доставка стирает у богатой поля, которых
|
||||
// сама не несёт.
|
||||
func TestMergePointsБеднаяТочкаНеСтираетБогатую(t *testing.T) {
|
||||
func TestMergeБеднаяТочкаНеСтираетБогатую(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
@@ -285,10 +285,10 @@ func TestMergePointsБеднаяТочкаНеСтираетБогатую(t *te
|
||||
poor := point(t, "heart_rate", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z",
|
||||
`{"Avg":60,"Min":55,"Max":70}`)
|
||||
|
||||
if _, err := st.MergePoints(ctx, []store.IncomingPoint{rich}, "d1"); err != nil {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{rich}}, store.DeliveryRef{ID: "d1"}); err != nil {
|
||||
t.Fatalf("первое слияние: %v", err)
|
||||
}
|
||||
stats, err := st.MergePoints(ctx, []store.IncomingPoint{poor}, "d2")
|
||||
stats, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{poor}}, store.DeliveryRef{ID: "d2"})
|
||||
if err != nil {
|
||||
t.Fatalf("второе слияние: %v", err)
|
||||
}
|
||||
@@ -311,7 +311,7 @@ func TestMergePointsБеднаяТочкаНеСтираетБогатую(t *te
|
||||
// Полнота — множество ключей, а не их число. Счётчик значащих полей давал
|
||||
// сохранённой точке 5 против 2 и стирал настоящее измерение безвозвратно:
|
||||
// восстановить его можно было бы только из сырого архива, пока он жив.
|
||||
func TestMergePointsПоляБезСодержанияНеСтираютИзмерение(t *testing.T) {
|
||||
func TestMergeПоляБезСодержанияНеСтираютИзмерение(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
@@ -322,10 +322,10 @@ func TestMergePointsПоляБезСодержанияНеСтираютИзме
|
||||
measured := point(t, "step_count", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z",
|
||||
`{"date":"2026-07-31 12:00:00 +0300","qty":123.4}`)
|
||||
|
||||
if _, err := st.MergePoints(ctx, []store.IncomingPoint{hollow}, "d1"); err != nil {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{hollow}}, store.DeliveryRef{ID: "d1"}); err != nil {
|
||||
t.Fatalf("первое слияние: %v", err)
|
||||
}
|
||||
stats, err := st.MergePoints(ctx, []store.IncomingPoint{measured}, "d2")
|
||||
stats, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{measured}}, store.DeliveryRef{ID: "d2"})
|
||||
if err != nil {
|
||||
t.Fatalf("второе слияние: %v", err)
|
||||
}
|
||||
@@ -345,7 +345,7 @@ func TestMergePointsПоляБезСодержанияНеСтираютИзме
|
||||
|
||||
// Поле с нулевым значением содержания не несёт, но и теряться не должно: при
|
||||
// равном множестве содержательных ключей выигрывает точка со всеми ключами.
|
||||
func TestMergePointsРавноеСодержаниеНеТеряетПоля(t *testing.T) {
|
||||
func TestMergeРавноеСодержаниеНеТеряетПоля(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
@@ -356,10 +356,10 @@ func TestMergePointsРавноеСодержаниеНеТеряетПоля(t *
|
||||
narrow := point(t, "heart_rate", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z",
|
||||
`{"date":"d","qty":12}`)
|
||||
|
||||
if _, err := st.MergePoints(ctx, []store.IncomingPoint{wide}, "d1"); err != nil {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{wide}}, store.DeliveryRef{ID: "d1"}); err != nil {
|
||||
t.Fatalf("первое слияние: %v", err)
|
||||
}
|
||||
if _, err := st.MergePoints(ctx, []store.IncomingPoint{narrow}, "d2"); err != nil {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{narrow}}, store.DeliveryRef{ID: "d2"}); err != nil {
|
||||
t.Fatalf("второе слияние: %v", err)
|
||||
}
|
||||
|
||||
@@ -375,7 +375,7 @@ func TestMergePointsРавноеСодержаниеНеТеряетПоля(t *
|
||||
// Несравнимые наборы полей на живом потоке не встретились ни разу (0 из 2 897
|
||||
// столкновений), поэтому объединение полей не реализовано. Взамен — наблюдение:
|
||||
// счётчик и координаты объекта, по которым событие можно будет разобрать.
|
||||
func TestMergePointsНесравнимыеНаборыСчитаются(t *testing.T) {
|
||||
func TestMergeНесравнимыеНаборыСчитаются(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
@@ -386,10 +386,10 @@ func TestMergePointsНесравнимыеНаборыСчитаются(t *test
|
||||
b := point(t, "blood_glucose", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z",
|
||||
`{"mealTime":"До еды"}`)
|
||||
|
||||
if _, err := st.MergePoints(ctx, []store.IncomingPoint{a}, "d1"); err != nil {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{a}}, store.DeliveryRef{ID: "d1"}); err != nil {
|
||||
t.Fatalf("первое слияние: %v", err)
|
||||
}
|
||||
stats, err := st.MergePoints(ctx, []store.IncomingPoint{b}, "d2")
|
||||
stats, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{b}}, store.DeliveryRef{ID: "d2"})
|
||||
if err != nil {
|
||||
t.Fatalf("второе слияние: %v", err)
|
||||
}
|
||||
@@ -420,7 +420,7 @@ func TestMergePointsНесравнимыеНаборыСчитаются(t *test
|
||||
|
||||
// Список координат упирается в потолок, счётчик — нет: обрезанный список
|
||||
// остаётся зацепкой для разбора, а масштаб события считает счётчик.
|
||||
func TestMergePointsСчётчикРастётПослеПотолкаКоординат(t *testing.T) {
|
||||
func TestMergeСчётчикРастётПослеПотолкаКоординат(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
@@ -434,10 +434,10 @@ func TestMergePointsСчётчикРастётПослеПотолкаКоорд
|
||||
second = append(second, point(t, "blood_glucose", "minute", at, at, `{"mealTime":"До еды"}`))
|
||||
}
|
||||
|
||||
if _, err := st.MergePoints(ctx, first, "d1"); err != nil {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: first}, store.DeliveryRef{ID: "d1"}); err != nil {
|
||||
t.Fatalf("первое слияние: %v", err)
|
||||
}
|
||||
stats, err := st.MergePoints(ctx, second, "d2")
|
||||
stats, err := st.Merge(ctx, store.Incoming{Points: second}, store.DeliveryRef{ID: "d2"})
|
||||
if err != nil {
|
||||
t.Fatalf("второе слияние: %v", err)
|
||||
}
|
||||
@@ -453,7 +453,7 @@ func TestMergePointsСчётчикРастётПослеПотолкаКоорд
|
||||
|
||||
// source нестабилен: то же измерение приезжает то с одним именем устройства,
|
||||
// то с другим. Он не входит в ключ и не считается полнотой.
|
||||
func TestMergePointsСменаИсточникаНеСоздаётВторуюТочку(t *testing.T) {
|
||||
func TestMergeСменаИсточникаНеСоздаётВторуюТочку(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
@@ -464,10 +464,10 @@ func TestMergePointsСменаИсточникаНеСоздаётВторуюТ
|
||||
b := point(t, "step_count", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z",
|
||||
`{"qty":1,"source":"Apple Watch Ultra 3"}`)
|
||||
|
||||
if _, err := st.MergePoints(ctx, []store.IncomingPoint{a}, "d1"); err != nil {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{a}}, store.DeliveryRef{ID: "d1"}); err != nil {
|
||||
t.Fatalf("первое слияние: %v", err)
|
||||
}
|
||||
if _, err := st.MergePoints(ctx, []store.IncomingPoint{b}, "d2"); err != nil {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{b}}, store.DeliveryRef{ID: "d2"}); err != nil {
|
||||
t.Fatalf("второе слияние: %v", err)
|
||||
}
|
||||
|
||||
@@ -483,7 +483,7 @@ func TestMergePointsСменаИсточникаНеСоздаётВторуюТ
|
||||
// Исход столкновения точек равной полноты обязан зависеть только от значений:
|
||||
// свёртка по журналу должна давать то же состояние, что приём в реальном
|
||||
// времени, а внутри одной доставки время приёма общее.
|
||||
func TestMergePointsРавнаяПолнотаРазрешаетсяДетерминированно(t *testing.T) {
|
||||
func TestMergeРавнаяПолнотаРазрешаетсяДетерминированно(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -494,7 +494,7 @@ func TestMergePointsРавнаяПолнотаРазрешаетсяДетерм
|
||||
winner := func(order []store.IncomingPoint) string {
|
||||
st := open(t)
|
||||
for _, p := range order {
|
||||
if _, err := st.MergePoints(ctx, []store.IncomingPoint{p}, "d"); err != nil {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{p}}, store.DeliveryRef{ID: "d"}); err != nil {
|
||||
t.Fatalf("слияние: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -515,7 +515,7 @@ func TestMergePointsРавнаяПолнотаРазрешаетсяДетерм
|
||||
// Содержимое точки хранится исходными байтами: пересборка повторной
|
||||
// сериализацией теряет литерал, и потеря не видна тестам, сравнивающим
|
||||
// разобранное с разобранным.
|
||||
func TestMergePointsХранитТочкуДословно(t *testing.T) {
|
||||
func TestMergeХранитТочкуДословно(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
@@ -525,7 +525,7 @@ func TestMergePointsХранитТочкуДословно(t *testing.T) {
|
||||
in := []store.IncomingPoint{
|
||||
point(t, "unknown", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z", raw),
|
||||
}
|
||||
if _, err := st.MergePoints(ctx, in, "d1"); err != nil {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: in}, store.DeliveryRef{ID: "d1"}); err != nil {
|
||||
t.Fatalf("слияние: %v", err)
|
||||
}
|
||||
|
||||
@@ -541,7 +541,7 @@ func TestMergePointsХранитТочкуДословно(t *testing.T) {
|
||||
// Тело доставки доходит до 42 МиБ, приходят они непрерывно и внахлёст.
|
||||
// Конкурентное слияние того же часа не имеет права терять точки: между
|
||||
// чтением и записью может вклиниться другая доставка.
|
||||
func TestMergePointsКонкурентноеСлияниеНеТеряетТочки(t *testing.T) {
|
||||
func TestMergeКонкурентноеСлияниеНеТеряетТочки(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
@@ -568,7 +568,7 @@ func TestMergePointsКонкурентноеСлияниеНеТеряетТоч
|
||||
Raw: json.RawMessage(`{"qty":` + itoa(minute) + `}`),
|
||||
},
|
||||
}
|
||||
if _, err := st.MergePoints(ctx, []store.IncomingPoint{p}, "d"); err != nil {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{p}}, store.DeliveryRef{ID: "d"}); err != nil {
|
||||
errs <- err
|
||||
return
|
||||
}
|
||||
@@ -607,7 +607,7 @@ func itoa(n int) string {
|
||||
// Изменение запечатанного часа — сигнал, а не отказ: данные пишутся всё равно,
|
||||
// но факт обязан дойти до владельца сервиса. Без счётчика допущение «глубже
|
||||
// такого-то порога досчёта не бывает» не получило бы ни одного наблюдения.
|
||||
func TestMergePointsИзменениеЗапечатанногоЧаса(t *testing.T) {
|
||||
func TestMergeИзменениеЗапечатанногоЧаса(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
@@ -617,7 +617,7 @@ func TestMergePointsИзменениеЗапечатанногоЧаса(t *test
|
||||
first := []store.IncomingPoint{
|
||||
point(t, "step_count", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z", `{"qty":1}`),
|
||||
}
|
||||
if _, err := st.MergePoints(ctx, first, "d1"); err != nil {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: first}, store.DeliveryRef{ID: "d1"}); err != nil {
|
||||
t.Fatalf("первое слияние: %v", err)
|
||||
}
|
||||
if err := st.MarkSealed(ctx, "step_count", "minute", hour, true); err != nil {
|
||||
@@ -627,7 +627,7 @@ func TestMergePointsИзменениеЗапечатанногоЧаса(t *test
|
||||
late := []store.IncomingPoint{
|
||||
point(t, "step_count", "minute", "2025-06-05T10:30:00Z", "2025-06-05T10:30:00Z", `{"qty":2}`),
|
||||
}
|
||||
stats, err := st.MergePoints(ctx, late, "d2")
|
||||
stats, err := st.Merge(ctx, store.Incoming{Points: late}, store.DeliveryRef{ID: "d2"})
|
||||
if err != nil {
|
||||
t.Fatalf("досчёт запечатанного часа: %v", err)
|
||||
}
|
||||
@@ -649,7 +649,7 @@ func TestMergePointsИзменениеЗапечатанногоЧаса(t *test
|
||||
|
||||
// Отмена посреди слияния не имеет права оставить половину: объект либо
|
||||
// прежний, либо полный.
|
||||
func TestMergePointsОтменаНеОставляетПоловины(t *testing.T) {
|
||||
func TestMergeОтменаНеОставляетПоловины(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
@@ -657,19 +657,30 @@ func TestMergePointsОтменаНеОставляетПоловины(t *testin
|
||||
base := []store.IncomingPoint{
|
||||
point(t, "step_count", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z", `{"qty":1}`),
|
||||
}
|
||||
if _, err := st.MergePoints(context.Background(), base, "d1"); err != nil {
|
||||
if _, err := st.Merge(context.Background(), store.Incoming{Points: base}, store.DeliveryRef{ID: "d1"}); err != nil {
|
||||
t.Fatalf("первое слияние: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
more := []store.IncomingPoint{
|
||||
point(t, "step_count", "minute", "2025-06-05T10:30:00Z", "2025-06-05T10:30:00Z", `{"qty":2}`),
|
||||
// Сущности идут в ТОЙ ЖЕ транзакции и пишутся ПОСЛЕ объектов, то есть на
|
||||
// той половине, где отмена вероятнее. Вынесение их во вторую транзакцию —
|
||||
// напрашивающаяся правка при жалобе на длину транзакции с маршрутом, и она
|
||||
// прошла бы зелёной, сломав «состояние пересобираемо»: доставка получила бы
|
||||
// failed при записанной тренировке.
|
||||
more := store.Incoming{
|
||||
Points: []store.IncomingPoint{
|
||||
point(t, "step_count", "minute", "2025-06-05T10:30:00Z", "2025-06-05T10:30:00Z", `{"qty":2}`),
|
||||
},
|
||||
Workouts: []store.IncomingEntity{workout(t, "w-отменённая", `{"id":"w-отменённая","qty":1}`)},
|
||||
}
|
||||
if _, err := st.MergePoints(ctx, more, "d2"); err == nil {
|
||||
if _, err := st.Merge(ctx, more, store.DeliveryRef{ID: "d2"}); err == nil {
|
||||
t.Fatal("слияние на отменённом контексте прошло успешно")
|
||||
}
|
||||
if _, err := st.Workout(context.Background(), "w-отменённая"); !errors.Is(err, store.ErrNotFound) {
|
||||
t.Errorf("тренировка отменённой доставки осталась в витрине: %v", err)
|
||||
}
|
||||
|
||||
b, err := st.Bucket(context.Background(), "step_count", "minute", ts(t, "2025-06-05T10:00:00Z"))
|
||||
if err != nil {
|
||||
@@ -704,7 +715,7 @@ func cycleTriple(t *testing.T) []store.IncomingPoint {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergePointsПовторнаяСвёрткаНеМеняетСостояние(t *testing.T) {
|
||||
func TestMergeПовторнаяСвёрткаНеМеняетСостояние(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
@@ -714,7 +725,7 @@ func TestMergePointsПовторнаяСвёрткаНеМеняетСостоя
|
||||
state := func() (string, string) {
|
||||
t.Helper()
|
||||
|
||||
if _, err := st.MergePoints(ctx, in, "d1"); err != nil {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: in}, store.DeliveryRef{ID: "d1"}); err != nil {
|
||||
t.Fatalf("слияние: %v", err)
|
||||
}
|
||||
b, err := st.Bucket(ctx, "heart_rate", "raw", ts(t, "2025-06-05T10:00:00Z"))
|
||||
@@ -738,7 +749,7 @@ func TestMergePointsПовторнаяСвёрткаНеМеняетСостоя
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergePointsИсходНеЗависитОтПерестановки(t *testing.T) {
|
||||
func TestMergeИсходНеЗависитОтПерестановки(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -752,7 +763,7 @@ func TestMergePointsИсходНеЗависитОтПерестановки(t *
|
||||
st := open(t)
|
||||
if split {
|
||||
for _, i := range order {
|
||||
if _, err := st.MergePoints(ctx, []store.IncomingPoint{in[i]}, "d"); err != nil {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{in[i]}}, store.DeliveryRef{ID: "d"}); err != nil {
|
||||
t.Fatalf("слияние: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -761,7 +772,7 @@ func TestMergePointsИсходНеЗависитОтПерестановки(t *
|
||||
for _, i := range order {
|
||||
batch = append(batch, in[i])
|
||||
}
|
||||
if _, err := st.MergePoints(ctx, batch, "d"); err != nil {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: batch}, store.DeliveryRef{ID: "d"}); err != nil {
|
||||
t.Fatalf("слияние: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -785,7 +796,7 @@ func TestMergePointsИсходНеЗависитОтПерестановки(t *
|
||||
// Точка, ни одно значение которой не несёт измерения, не должна вытеснять
|
||||
// настоящее измерение. Раньше вытесняла: множества содержательных ключей
|
||||
// равны, и решал второй разряд — по ключам, а не по содержанию.
|
||||
func TestMergePointsПадингНеВытесняетИзмерение(t *testing.T) {
|
||||
func TestMergeПадингНеВытесняетИзмерение(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -808,7 +819,7 @@ func TestMergePointsПадингНеВытесняетИзмерение(t *test
|
||||
point(t, "heart_rate", "raw", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z", real),
|
||||
point(t, "heart_rate", "raw", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z", c.junk),
|
||||
}
|
||||
stats, err := st.MergePoints(ctx, in, "d1")
|
||||
stats, err := st.Merge(ctx, store.Incoming{Points: in}, store.DeliveryRef{ID: "d1"})
|
||||
if err != nil {
|
||||
t.Fatalf("слияние: %v", err)
|
||||
}
|
||||
@@ -824,7 +835,7 @@ func TestMergePointsПадингНеВытесняетИзмерение(t *test
|
||||
// Имя метрики приходит из тела доставки дословно и ничем не ограничено.
|
||||
// Без обрезки одна доставка порождает WARN-строку в десятки мегабайт и
|
||||
// вытесняет из ротации логов всю недавнюю историю.
|
||||
func TestMergePointsИмяМетрикиВКоординатеОбрезано(t *testing.T) {
|
||||
func TestMergeИмяМетрикиВКоординатеОбрезано(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
@@ -835,7 +846,7 @@ func TestMergePointsИмяМетрикиВКоординатеОбрезано(t
|
||||
point(t, huge, "raw", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z", `{"qty":1}`),
|
||||
point(t, huge, "raw", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z", `{"qty":2}`),
|
||||
}
|
||||
stats, err := st.MergePoints(ctx, in, "d1")
|
||||
stats, err := st.Merge(ctx, store.Incoming{Points: in}, store.DeliveryRef{ID: "d1"})
|
||||
if err != nil {
|
||||
t.Fatalf("слияние: %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user