store: при равной полноте точек побеждает пришедшая доставка
- байтовый порядок канонических форм остался тай-брейком только внутри одной доставки: на живом корпусе он решал 98,8% спорных координат и системно хранил меньшее значение, из-за чего step_count терял род и verify:archive был красным - правило перестало быть коммутативным осознанно, поэтому порядок свёртки приведён к журнальному: проход воркера прекращается на отложенной доставке, а свёртка вне порядка журнала пишет WARN - заведены счётчики PointsHeld и PointsErased — удержание полнотой и единственное направление, в котором правило теряет содержание
This commit is contained in:
+331
-30
@@ -351,10 +351,16 @@ func TestMergeРавноеСодержаниеНеТеряетПоля(t *testin
|
||||
st := open(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// Значение общего содержательного ключа СОВПАДАЕТ — таково условие второго
|
||||
// разряда в спеке: он сравнивает все ключи только там, где содержание
|
||||
// сошлось. Прежняя редакция теста давала `qty` разные, второй разряд при
|
||||
// этом не запускался вовсе, и точка с `Min`/`Max` выигрывала по случайности
|
||||
// байтового порядка (`M` сортируется раньше `d`). Зелёный тест проверял не
|
||||
// то требование, которое называл.
|
||||
wide := point(t, "heart_rate", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z",
|
||||
`{"date":"d","qty":10,"Min":0,"Max":0}`)
|
||||
narrow := point(t, "heart_rate", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z",
|
||||
`{"date":"d","qty":12}`)
|
||||
`{"date":"d","qty":10}`)
|
||||
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{wide}}, store.DeliveryRef{ID: "d1"}); err != nil {
|
||||
t.Fatalf("первое слияние: %v", err)
|
||||
@@ -372,6 +378,86 @@ func TestMergeРавноеСодержаниеНеТеряетПоля(t *testin
|
||||
}
|
||||
}
|
||||
|
||||
// Обратная сторона того же разряда, и она названа ценой изменения: когда
|
||||
// значения общих содержательных ключей РАЗОШЛИСЬ, полнота ответа не даёт по
|
||||
// построению («надмножество имён о полноте не говорит ничего»), и пришедшая
|
||||
// точка побеждает вместе с потерей пустых ключей сохранённой. Прежде исход
|
||||
// здесь решался байтовым порядком, то есть был случайным.
|
||||
func TestMergeРазошедшиесяЗначенияНеУдерживаютсяПустымиКлючами(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
ctx := context.Background()
|
||||
|
||||
wide := point(t, "heart_rate", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z",
|
||||
`{"date":"d","qty":10,"Min":0,"Max":0}`)
|
||||
narrow := point(t, "heart_rate", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z",
|
||||
`{"date":"d","qty":12}`)
|
||||
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{wide}}, store.DeliveryRef{ID: "d1"}); err != nil {
|
||||
t.Fatalf("первое слияние: %v", err)
|
||||
}
|
||||
stats, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{narrow}}, store.DeliveryRef{ID: "d2"})
|
||||
if err != nil {
|
||||
t.Fatalf("второе слияние: %v", err)
|
||||
}
|
||||
|
||||
b, err := st.Bucket(ctx, "heart_rate", "minute", ts(t, "2025-06-05T10:00:00Z"))
|
||||
if err != nil {
|
||||
t.Fatalf("чтение: %v", err)
|
||||
}
|
||||
if got := string(b.Points[0].Raw); got != string(narrow.Raw) {
|
||||
t.Errorf("в витрине %s, ожидалась пришедшая точка %s", got, narrow.Raw)
|
||||
}
|
||||
if stats.Overwrites != 1 {
|
||||
t.Errorf("перезаписей %d, ожидалась 1", stats.Overwrites)
|
||||
}
|
||||
if stats.PointsHeld != 0 {
|
||||
t.Errorf("удержаний %d: пришедшая победила, удерживать было нечего", stats.PointsHeld)
|
||||
}
|
||||
}
|
||||
|
||||
// Полнота сильнее происхождения: пришедшая точка, теряющая содержательный ключ
|
||||
// сохранённой при совпадающих значениях, проигрывает — и это единственный
|
||||
// случай, когда пришедшая вообще проигрывает. Счётчик удержаний считает ровно
|
||||
// его и отдельно от перезаписей.
|
||||
func TestMergeПолнотаСильнееПроисхождения(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
ctx := context.Background()
|
||||
|
||||
rich := point(t, "heart_rate", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z",
|
||||
`{"Avg":72,"Min":60,"Max":90,"context":"rest"}`)
|
||||
poor := point(t, "heart_rate", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z",
|
||||
`{"Avg":72,"Min":60,"Max":90}`)
|
||||
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{rich}}, store.DeliveryRef{ID: "d1"}); err != nil {
|
||||
t.Fatalf("первое слияние: %v", err)
|
||||
}
|
||||
stats, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{poor}}, store.DeliveryRef{ID: "d2"})
|
||||
if err != nil {
|
||||
t.Fatalf("второе слияние: %v", err)
|
||||
}
|
||||
|
||||
b, err := st.Bucket(ctx, "heart_rate", "minute", ts(t, "2025-06-05T10:00:00Z"))
|
||||
if err != nil {
|
||||
t.Fatalf("чтение: %v", err)
|
||||
}
|
||||
if got := string(b.Points[0].Raw); got != string(rich.Raw) {
|
||||
t.Errorf("в витрине %s: пришедшая бедная точка стёрла содержательный ключ", got)
|
||||
}
|
||||
if stats.PointsHeld != 1 {
|
||||
t.Errorf("удержаний %d, ожидалось 1", stats.PointsHeld)
|
||||
}
|
||||
if len(stats.PointsHeldAt) != 1 {
|
||||
t.Fatalf("координат удержания %d, ожидалась 1", len(stats.PointsHeldAt))
|
||||
}
|
||||
if stats.PointsHeldAt[0].Metric != "heart_rate" || stats.PointsHeldAt[0].Layer != "minute" {
|
||||
t.Errorf("координаты удержания %+v: не те", stats.PointsHeldAt[0])
|
||||
}
|
||||
}
|
||||
|
||||
// Несравнимые наборы полей на живом потоке не встретились ни разу (0 из 2 897
|
||||
// столкновений), поэтому объединение полей не реализовано. Взамен — наблюдение:
|
||||
// счётчик и координаты объекта, по которым событие можно будет разобрать.
|
||||
@@ -480,10 +566,10 @@ func TestMergeСменаИсточникаНеСоздаётВторуюТочк
|
||||
}
|
||||
}
|
||||
|
||||
// Исход столкновения точек равной полноты обязан зависеть только от значений:
|
||||
// свёртка по журналу должна давать то же состояние, что приём в реальном
|
||||
// времени, а внутри одной доставки время приёма общее.
|
||||
func TestMergeРавнаяПолнотаРазрешаетсяДетерминированно(t *testing.T) {
|
||||
// Внутри ОДНОЙ доставки провенанс общий, различать точки нечем, а порядок
|
||||
// элементов в JSON-массиве от HAE нестабилен — поэтому здесь исход обязан
|
||||
// остаться функцией множества и решаться порядком канонических форм.
|
||||
func TestMergeРавнаяПолнотаВнутриДоставкиНеЗависитОтПорядка(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -491,12 +577,10 @@ func TestMergeРавнаяПолнотаРазрешаетсяДетермини
|
||||
a := point(t, "step_count", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z", `{"qty":1}`)
|
||||
b := point(t, "step_count", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z", `{"qty":2}`)
|
||||
|
||||
winner := func(order []store.IncomingPoint) string {
|
||||
winner := func(batch []store.IncomingPoint) string {
|
||||
st := open(t)
|
||||
for _, p := range order {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{p}}, store.DeliveryRef{ID: "d"}); err != nil {
|
||||
t.Fatalf("слияние: %v", err)
|
||||
}
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: batch}, store.DeliveryRef{ID: "d"}); err != nil {
|
||||
t.Fatalf("слияние: %v", err)
|
||||
}
|
||||
got, err := st.Bucket(ctx, "step_count", "minute", ts(t, "2025-06-05T10:00:00Z"))
|
||||
if err != nil {
|
||||
@@ -508,7 +592,119 @@ func TestMergeРавнаяПолнотаРазрешаетсяДетермини
|
||||
forward := winner([]store.IncomingPoint{a, b})
|
||||
backward := winner([]store.IncomingPoint{b, a})
|
||||
if forward != backward {
|
||||
t.Errorf("исход зависит от порядка доставок: %s против %s", forward, backward)
|
||||
t.Errorf("исход внутри доставки зависит от порядка элементов: %s против %s", forward, backward)
|
||||
}
|
||||
if forward != string(a.Raw) {
|
||||
t.Errorf("внутри доставки победила %s, ожидался минимум канонической формы %s", forward, a.Raw)
|
||||
}
|
||||
}
|
||||
|
||||
// А МЕЖДУ доставками побеждает пришедшая, и это осознанная потеря
|
||||
// коммутативности: байтовый порядок системно хранил меньшее значение и
|
||||
// стоил `step_count` его рода. Витрина остаётся свёрткой журнала потому, что
|
||||
// порядок свёртки приведён к порядку журнала, а не потому, что правило
|
||||
// безразлично к порядку.
|
||||
func TestMergeРавнаяПолнотаМеждуДоставкамиБерётПришедшую(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
a := point(t, "step_count", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z", `{"qty":1}`)
|
||||
b := point(t, "step_count", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z", `{"qty":2}`)
|
||||
|
||||
last := func(order []store.IncomingPoint) string {
|
||||
st := open(t)
|
||||
for i, p := range order {
|
||||
ref := store.DeliveryRef{ID: fmt.Sprintf("d%d", i)}
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{p}}, ref); err != nil {
|
||||
t.Fatalf("слияние: %v", err)
|
||||
}
|
||||
}
|
||||
got, err := st.Bucket(ctx, "step_count", "minute", ts(t, "2025-06-05T10:00:00Z"))
|
||||
if err != nil {
|
||||
t.Fatalf("чтение: %v", err)
|
||||
}
|
||||
return string(got.Points[0].Raw)
|
||||
}
|
||||
|
||||
if got := last([]store.IncomingPoint{a, b}); got != string(b.Raw) {
|
||||
t.Errorf("после доставок a,b в витрине %s, ожидалась пришедшая последней %s", got, b.Raw)
|
||||
}
|
||||
// Обратный порядок даёт обратный исход — ровно то свойство, ради которого
|
||||
// заведён барьер журнального порядка в воркере.
|
||||
if got := last([]store.IncomingPoint{b, a}); got != string(a.Raw) {
|
||||
t.Errorf("после доставок b,a в витрине %s, ожидалась пришедшая последней %s", got, a.Raw)
|
||||
}
|
||||
}
|
||||
|
||||
// Повторная присылка того же содержимого другими байтами ничего не переписывает:
|
||||
// схлопывание поднимает флаг происхождения, но байты в витрине остаются
|
||||
// сохранённые, и хеш-детектор гасит запись.
|
||||
func TestMergeПовторКаноническиТойЖеТочкиНеПереписывает(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
ctx := context.Background()
|
||||
|
||||
first := point(t, "step_count", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z",
|
||||
`{"date":"d","qty":10}`)
|
||||
// Тот же смысл, другие байты: порядок ключей у HAE нестабилен.
|
||||
again := point(t, "step_count", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z",
|
||||
`{"qty":10,"date":"d"}`)
|
||||
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{first}}, store.DeliveryRef{ID: "d1"}); err != nil {
|
||||
t.Fatalf("первое слияние: %v", err)
|
||||
}
|
||||
stats, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{again}}, store.DeliveryRef{ID: "d2"})
|
||||
if err != nil {
|
||||
t.Fatalf("второе слияние: %v", err)
|
||||
}
|
||||
if stats.Overwrites != 0 {
|
||||
t.Errorf("перезаписей %d: дребезг порядка ключей посчитан столкновением", stats.Overwrites)
|
||||
}
|
||||
if stats.Unchanged != 1 {
|
||||
t.Errorf("неизменившихся объектов %d, ожидался 1: объект переписан зря", stats.Unchanged)
|
||||
}
|
||||
|
||||
b, err := st.Bucket(ctx, "step_count", "minute", ts(t, "2025-06-05T10:00:00Z"))
|
||||
if err != nil {
|
||||
t.Fatalf("чтение: %v", err)
|
||||
}
|
||||
if got := string(b.Points[0].Raw); got != string(first.Raw) {
|
||||
t.Errorf("в витрине %s, ожидались сохранённые байты %s", got, first.Raw)
|
||||
}
|
||||
}
|
||||
|
||||
// Точка, канонически совпавшая с сохранённой, представляет доставку наравне с
|
||||
// остальными её точками: иначе она проигрывала бы соседу по телу, которого
|
||||
// обязана обойти по байтам, — то есть «внутри доставки решают байты» ломалось
|
||||
// бы ровно тогда, когда доставка ничего не изменила.
|
||||
func TestMergeСхлопываниеПоднимаетПроисхождение(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
stored := point(t, "step_count", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z", `{"qty":1}`)
|
||||
same := point(t, "step_count", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z", `{"qty":1}`)
|
||||
other := point(t, "step_count", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z", `{"qty":2}`)
|
||||
|
||||
st := open(t)
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{stored}}, store.DeliveryRef{ID: "d1"}); err != nil {
|
||||
t.Fatalf("первое слияние: %v", err)
|
||||
}
|
||||
// Вторая доставка несёт обе формы: совпавшую с сохранённой и другую.
|
||||
// Победить обязана меньшая каноническая форма, то есть совпавшая, — а не
|
||||
// вторая только потому, что у первой не поднялся флаг.
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{same, other}}, store.DeliveryRef{ID: "d2"}); err != nil {
|
||||
t.Fatalf("второе слияние: %v", err)
|
||||
}
|
||||
|
||||
b, err := st.Bucket(ctx, "step_count", "minute", ts(t, "2025-06-05T10:00:00Z"))
|
||||
if err != nil {
|
||||
t.Fatalf("чтение: %v", err)
|
||||
}
|
||||
if got := string(b.Points[0].Raw); got != string(stored.Raw) {
|
||||
t.Errorf("в витрине %s, ожидался минимум канонической формы доставки %s", got, stored.Raw)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -755,24 +951,57 @@ func TestMergeИсходНеЗависитОтПерестановки(t *testin
|
||||
ctx := context.Background()
|
||||
in := cycleTriple(t)
|
||||
|
||||
// Все шесть перестановок тройки, и вдобавок разбиение на разные доставки:
|
||||
// в живом приёме точки приходят порознь, в пересборке — вместе.
|
||||
// Все шесть перестановок тройки ВНУТРИ одной доставки. Разбиение на разные
|
||||
// доставки сюда больше не входит намеренно: между доставками правило
|
||||
// перестало быть коммутативным — при равной полноте побеждает пришедшая, —
|
||||
// и требовать здесь независимости от порядка значило бы требовать отката
|
||||
// починки. Что осталось верным между доставками, проверяют
|
||||
// `TestMergeРавнаяПолнотаМеждуДоставкамиБерётПришедшую` (исход = последняя
|
||||
// доставка) и прогон сходимости живого пути с пересборкой в
|
||||
// `internal/replay`.
|
||||
perms := [][]int{{0, 1, 2}, {0, 2, 1}, {1, 0, 2}, {1, 2, 0}, {2, 0, 1}, {2, 1, 0}}
|
||||
|
||||
winner := func(order []int, split bool) string {
|
||||
winner := func(order []int) string {
|
||||
st := open(t)
|
||||
if split {
|
||||
for _, i := range order {
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{in[i]}}, store.DeliveryRef{ID: "d"}); err != nil {
|
||||
t.Fatalf("слияние: %v", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
batch := make([]store.IncomingPoint, 0, len(order))
|
||||
for _, i := range order {
|
||||
batch = append(batch, in[i])
|
||||
}
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: batch}, store.DeliveryRef{ID: "d"}); err != nil {
|
||||
batch := make([]store.IncomingPoint, 0, len(order))
|
||||
for _, i := range order {
|
||||
batch = append(batch, in[i])
|
||||
}
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: batch}, store.DeliveryRef{ID: "d"}); err != nil {
|
||||
t.Fatalf("слияние: %v", err)
|
||||
}
|
||||
b, err := st.Bucket(ctx, "heart_rate", "raw", ts(t, "2025-06-05T10:00:00Z"))
|
||||
if err != nil {
|
||||
t.Fatalf("чтение: %v", err)
|
||||
}
|
||||
return string(b.Points[0].Raw)
|
||||
}
|
||||
|
||||
want := winner(perms[0])
|
||||
for _, p := range perms {
|
||||
if got := winner(p); got != want {
|
||||
t.Errorf("перестановка %v дала %s, ожидалось %s", p, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Та же тройка, разложенная по РАЗНЫМ доставкам: исход теперь зависит от
|
||||
// порядка, и это не дефект, а объявленная цена. Проверяется другое и не менее
|
||||
// важное — что зависимость ДЕТЕРМИНИРОВАНА: один и тот же порядок журнала
|
||||
// обязан давать один и тот же исход, иначе витрина перестала бы быть свёрткой
|
||||
// журнала вовсе, а не только функцией его порядка.
|
||||
func TestMergeПорядокДоставокДаётВоспроизводимыйИсход(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
in := cycleTriple(t)
|
||||
perms := [][]int{{0, 1, 2}, {0, 2, 1}, {1, 0, 2}, {1, 2, 0}, {2, 0, 1}, {2, 1, 0}}
|
||||
|
||||
winner := func(order []int) string {
|
||||
st := open(t)
|
||||
for n, i := range order {
|
||||
ref := store.DeliveryRef{ID: fmt.Sprintf("d%d", n)}
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{in[i]}}, ref); err != nil {
|
||||
t.Fatalf("слияние: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -783,11 +1012,11 @@ func TestMergeИсходНеЗависитОтПерестановки(t *testin
|
||||
return string(b.Points[0].Raw)
|
||||
}
|
||||
|
||||
want := winner(perms[0], false)
|
||||
for _, p := range perms {
|
||||
for _, split := range []bool{false, true} {
|
||||
if got := winner(p, split); got != want {
|
||||
t.Errorf("перестановка %v (порознь=%v) дала %s, ожидалось %s", p, split, got, want)
|
||||
first := winner(p)
|
||||
for pass := 2; pass <= 3; pass++ {
|
||||
if got := winner(p); got != first {
|
||||
t.Errorf("порядок %v дал %s, а прежде %s: исход невоспроизводим", p, got, first)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -857,3 +1086,75 @@ func TestMergeИмяМетрикиВКоординатеОбрезано(t *test
|
||||
t.Errorf("имя метрики в координате %d байт — оно уедет в лог как есть", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Единственное направление, в котором новое правило теряет содержание:
|
||||
// значения общих содержательных ключей разошлись, разряд полноты погас, и
|
||||
// победила пришедшая точка, у которой содержательного ключа нет.
|
||||
//
|
||||
// До смены тай-брейка исход здесь решал байтовый порядок — то есть та же
|
||||
// потеря случалась по жребию и так же молча. Разница в том, что теперь она
|
||||
// детерминирована и СЧИТАЕТСЯ.
|
||||
func TestMergeПришедшаяУнесшаяСодержаниеСчитаетсяОтдельно(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// `rem` — содержательный ключ сохранённой; у пришедшей его нет, а значение
|
||||
// общего ключа `asleep` разошлось, поэтому Relate объявляет пару равнополной.
|
||||
stored := point(t, "sleep_analysis_summary", "day", "2025-06-05T00:00:00Z", "2025-06-05T00:00:00Z",
|
||||
`{"date":"d","asleep":7.5,"rem":1.2}`)
|
||||
arriving := point(t, "sleep_analysis_summary", "day", "2025-06-05T00:00:00Z", "2025-06-05T00:00:00Z",
|
||||
`{"date":"d","asleep":7.4}`)
|
||||
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{stored}}, store.DeliveryRef{ID: "d1"}); err != nil {
|
||||
t.Fatalf("первое слияние: %v", err)
|
||||
}
|
||||
stats, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{arriving}}, store.DeliveryRef{ID: "d2"})
|
||||
if err != nil {
|
||||
t.Fatalf("второе слияние: %v", err)
|
||||
}
|
||||
|
||||
b, err := st.Bucket(ctx, "sleep_analysis_summary", "day", ts(t, "2025-06-05T00:00:00Z"))
|
||||
if err != nil {
|
||||
t.Fatalf("чтение: %v", err)
|
||||
}
|
||||
if got := string(b.Points[0].Raw); got != string(arriving.Raw) {
|
||||
t.Fatalf("в витрине %s, ожидалась пришедшая точка %s", got, arriving.Raw)
|
||||
}
|
||||
if stats.PointsErased != 1 {
|
||||
t.Errorf("потерь содержания %d, ожидалась 1: направление, в котором правило теряет, осталось невидимым", stats.PointsErased)
|
||||
}
|
||||
if len(stats.PointsErasedAt) != 1 {
|
||||
t.Errorf("координат потери %d, ожидалась 1", len(stats.PointsErasedAt))
|
||||
}
|
||||
if stats.PointsHeld != 0 {
|
||||
t.Errorf("удержаний %d: победила пришедшая, удерживать было нечего", stats.PointsHeld)
|
||||
}
|
||||
}
|
||||
|
||||
// Обратное: пришедшая победила, ничего не унеся, — счётчик молчит. Без этой
|
||||
// пары счётчик потерь был бы неотличим от счётчика перезаписей.
|
||||
func TestMergeПришедшаяБезПотериСодержанияНеСчитается(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
st := open(t)
|
||||
ctx := context.Background()
|
||||
|
||||
first := point(t, "step_count", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z", `{"date":"d","qty":1}`)
|
||||
second := point(t, "step_count", "minute", "2025-06-05T10:00:00Z", "2025-06-05T10:00:00Z", `{"date":"d","qty":2}`)
|
||||
|
||||
if _, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{first}}, store.DeliveryRef{ID: "d1"}); err != nil {
|
||||
t.Fatalf("первое слияние: %v", err)
|
||||
}
|
||||
stats, err := st.Merge(ctx, store.Incoming{Points: []store.IncomingPoint{second}}, store.DeliveryRef{ID: "d2"})
|
||||
if err != nil {
|
||||
t.Fatalf("второе слияние: %v", err)
|
||||
}
|
||||
if stats.PointsErased != 0 {
|
||||
t.Errorf("потерь содержания %d, ожидалось 0: наборы ключей совпадают, терять нечего", stats.PointsErased)
|
||||
}
|
||||
if stats.Overwrites != 1 {
|
||||
t.Errorf("перезаписей %d, ожидалась 1", stats.Overwrites)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user