отработаны находки ревью кода
Триаж свёл 62 сырые находки девяти проходов к 33 причинам: 3 блокера, 4 «сейчас», 2 развилки. Все закрыты регрессионными тестами. - схема точки сна определяется по самой точке, а не по индексу в исходном массиве: одна пропущенная точка меняла эпизод и сводку местами - доставка сворачивается одной транзакцией: частичное состояние было недетерминированным (восемь прогонов — семь состояний) - граница размера на распакованном теле: 400 КиБ gzip разворачивались в 400 МиБ мимо max_body_mb - столкновение — расхождение канонических форм, а не байтов; WARN с координатами объекта; payload без HTML-экранирования - выравнивание по местной метке: получасовые зоны уводили часовую выгрузку в minute - доставка из одних суточных сводок больше не отвергается целиком - единицы не переписываются молча; счётчик считает сохранённые точки - все выходы Fold логируются, исход пишется на переживающем отмену контексте Четыре развилки вынесены блокерами в беклог.
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
package fold_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.vakhrushev.me/av/healthlog/internal/archive"
|
||||
"git.vakhrushev.me/av/healthlog/internal/fold"
|
||||
"git.vakhrushev.me/av/healthlog/internal/store"
|
||||
)
|
||||
|
||||
// Данные о здоровье чувствительнее токенов, и требование «значения точек не в
|
||||
// логах» до сих пор не проверялось ничем: все тестовые логгеры выбрасывали
|
||||
// записи. Тест ловит записи и смотрит на них.
|
||||
func TestFoldНеПишетЗначенийВЛог(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var buf bytes.Buffer
|
||||
log := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo}))
|
||||
|
||||
dir := t.TempDir()
|
||||
arch, err := archive.New(filepath.Join(dir, "raw"))
|
||||
if err != nil {
|
||||
t.Fatalf("архив: %v", err)
|
||||
}
|
||||
st, err := store.Open(filepath.Join(dir, "healthlog.db"))
|
||||
if err != nil {
|
||||
t.Fatalf("база: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = st.Close() })
|
||||
|
||||
f := fold.New(arch, st, 0, log)
|
||||
|
||||
// Значения и имя устройства выбраны так, чтобы их нельзя было спутать ни с
|
||||
// чем: если они окажутся в логе, это будет видно.
|
||||
body := []byte(`{"data":{"metrics":[{"name":"step_count","units":"count","data":[` +
|
||||
`{"date":"2025-06-05 10:00:00 +0300","qty":424242.7,"source":"Секретные Часы Антона"},` +
|
||||
`{"date":"2025-06-05 10:01:00 +0300","qty":313131.9,"source":"Секретные Часы Антона"},` +
|
||||
`{"date":"2025-06-05 10:02:00 +0300","qty":151515.1,"source":"Секретные Часы Антона"}` +
|
||||
`]}]}}`)
|
||||
|
||||
deliver(t, arch, st, "d1", "Minutes", "auto-1", body)
|
||||
if _, err := f.Fold(context.Background(), "d1"); err != nil {
|
||||
t.Fatalf("свёртка: %v", err)
|
||||
}
|
||||
|
||||
logged := buf.String()
|
||||
if logged == "" {
|
||||
t.Fatal("свёртка не записала ни одной строки — чекпоинт молчит")
|
||||
}
|
||||
|
||||
for _, secret := range []string{"424242", "313131", "151515", "Секретные Часы"} {
|
||||
if strings.Contains(logged, secret) {
|
||||
t.Errorf("в логе оказалось %q:\n%s", secret, logged)
|
||||
}
|
||||
}
|
||||
|
||||
// И одновременно — чекпоинт обязан нести счётчики, иначе он бесполезен.
|
||||
var rec map[string]any
|
||||
for line := range strings.SplitSeq(strings.TrimSpace(logged), "\n") {
|
||||
if err := json.Unmarshal([]byte(line), &rec); err != nil {
|
||||
t.Fatalf("строка лога не JSON: %v", err)
|
||||
}
|
||||
if rec["msg"] == "delivery folded" {
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, attr := range []string{"delivery_id", "points", "stored", "buckets", "layer", "skipped"} {
|
||||
if _, ok := rec[attr]; !ok {
|
||||
t.Errorf("в записи нет атрибута %q: %v", attr, rec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Доставка, у которой отброшены ВСЕ точки, — это сломавшийся формат, а не
|
||||
// штатная работа. Без WARN смена формата метки выглядела бы как здоровый
|
||||
// поток: 200, parsed, INFO, points=0.
|
||||
func TestFoldВсеТочкиОтброшеныДаётWarn(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var buf bytes.Buffer
|
||||
log := slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo}))
|
||||
|
||||
dir := t.TempDir()
|
||||
arch, err := archive.New(filepath.Join(dir, "raw"))
|
||||
if err != nil {
|
||||
t.Fatalf("архив: %v", err)
|
||||
}
|
||||
st, err := store.Open(filepath.Join(dir, "healthlog.db"))
|
||||
if err != nil {
|
||||
t.Fatalf("база: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = st.Close() })
|
||||
|
||||
f := fold.New(arch, st, 0, log)
|
||||
|
||||
// Метки в формате, которого разбор не знает: HAE сменил формат.
|
||||
body := []byte(`{"data":{"metrics":[{"name":"step_count","units":"count","data":[` +
|
||||
`{"date":"2025-06-05T10:00:00Z","qty":1},{"date":"2025-06-05T10:01:00Z","qty":2}` +
|
||||
`]}]}}`)
|
||||
|
||||
deliver(t, arch, st, "d1", "Minutes", "auto-1", body)
|
||||
if _, err := f.Fold(context.Background(), "d1"); err != nil {
|
||||
t.Fatalf("свёртка: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(buf.String(), `"level":"WARN"`) {
|
||||
t.Errorf("доставка без единой сохранённой точки записана не как WARN:\n%s", buf.String())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user