непокрытые секции доставки видны в статусе разбора
- половина потока (50 доставок из 104) не несёт metrics вовсе и до сих пор числилась parsed: ретеншен, поверив статусу, срезал бы тела stateOfMind, которых в экспорте Apple нет - разбор перечисляет верхнеуровневые ключи data, непокрытые проглатываются декодированием: тело 40 МиБ из непокрытой секции удерживает 0 МиБ - статус partial и колонка delivery.uncovered_sections; миграция переводит прежние parsed в pending — им верить нельзя - витрина не изменилась: отпечаток совпал с прогоном до изменения
This commit is contained in:
@@ -3,11 +3,14 @@ package hae_test
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"git.vakhrushev.me/av/healthlog/internal/hae"
|
||||
)
|
||||
@@ -544,3 +547,157 @@ func FuzzParse(f *testing.F) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Половина живого потока состоит из непокрытых секций целиком (48 доставок из
|
||||
// 99: workouts и stateOfMind). Без списка они неотличимы от разобранной
|
||||
// доставки с пустой секцией метрик, и ретеншен, ориентируясь на статус, срезал
|
||||
// бы тела, которые для stateOfMind единственный источник.
|
||||
func TestParseПеречисляетНепокрытыеСекции(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
res, err := hae.Parse(load(t, "uncovered_sections.json"), hae.Meta{})
|
||||
if err != nil {
|
||||
t.Fatalf("разбор: %v", err)
|
||||
}
|
||||
|
||||
want := []string{"stateOfMind", "workouts"}
|
||||
if !slices.Equal(res.Uncovered, want) {
|
||||
t.Errorf("непокрытые %v, ожидались %v", res.Uncovered, want)
|
||||
}
|
||||
if len(res.Points) == 0 {
|
||||
t.Error("точки метрик обязаны сохраниться: непокрытая секция рядом им не мешает")
|
||||
}
|
||||
if res.UncoveredDropped != 0 {
|
||||
t.Errorf("отброшено %d имён, ожидалось 0", res.UncoveredDropped)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseНепокрытыеСекцииГраницыИДетерминизм(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("доставка из одной непокрытой секции", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
res, err := hae.Parse([]byte(`{"data":{"stateOfMind":[{"x":1}]}}`), hae.Meta{})
|
||||
if err != nil {
|
||||
t.Fatalf("разбор: %v", err)
|
||||
}
|
||||
if !slices.Equal(res.Uncovered, []string{"stateOfMind"}) {
|
||||
t.Errorf("непокрытые %v", res.Uncovered)
|
||||
}
|
||||
if len(res.Points) != 0 {
|
||||
t.Errorf("точек %d, ожидалось 0", len(res.Points))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("доставка из одних метрик списка не даёт", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
res, err := hae.Parse([]byte(`{"data":{"metrics":[]}}`), hae.Meta{})
|
||||
if err != nil {
|
||||
t.Fatalf("разбор: %v", err)
|
||||
}
|
||||
if len(res.Uncovered) != 0 {
|
||||
t.Errorf("непокрытые %v, ожидался пустой список", res.Uncovered)
|
||||
}
|
||||
})
|
||||
|
||||
// Порядок ключей в JSON от HAE нестабилен, а список уезжает в базу и
|
||||
// сравнивается между доставками: зависящий от порядка на проводе список
|
||||
// сравнивать нельзя.
|
||||
t.Run("порядок и повторы не влияют", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
bodies := []string{
|
||||
`{"data":{"workouts":[],"stateOfMind":[],"ecg":[]}}`,
|
||||
`{"data":{"ecg":[],"workouts":[],"stateOfMind":[]}}`,
|
||||
`{"data":{"stateOfMind":[],"ecg":[],"workouts":[],"ecg":[]}}`,
|
||||
}
|
||||
want := []string{"ecg", "stateOfMind", "workouts"}
|
||||
for _, b := range bodies {
|
||||
res, err := hae.Parse([]byte(b), hae.Meta{})
|
||||
if err != nil {
|
||||
t.Fatalf("разбор %s: %v", b, err)
|
||||
}
|
||||
if !slices.Equal(res.Uncovered, want) {
|
||||
t.Errorf("тело %s дало %v, ожидалось %v", b, res.Uncovered, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Повтор ключа metrics JSON допускает; секции объединяются, а не побеждает
|
||||
// последняя — терять точки молча нельзя.
|
||||
t.Run("повтор metrics объединяет секции", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const body = `{"data":{"metrics":[{"name":"a","data":[]}],"metrics":[{"name":"b","data":[]}]}}`
|
||||
res, err := hae.Parse([]byte(body), hae.Meta{})
|
||||
if err != nil {
|
||||
t.Fatalf("разбор: %v", err)
|
||||
}
|
||||
if res.Metrics != 2 {
|
||||
t.Errorf("метрик %d, ожидалось 2 — секции обязаны объединиться", res.Metrics)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("границы списка видны", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(`{"data":{`)
|
||||
for i := range 40 {
|
||||
if i > 0 {
|
||||
sb.WriteByte(',')
|
||||
}
|
||||
fmt.Fprintf(&sb, `"s%02d":[]`, i)
|
||||
}
|
||||
sb.WriteString(`}}`)
|
||||
|
||||
res, err := hae.Parse([]byte(sb.String()), hae.Meta{})
|
||||
if err != nil {
|
||||
t.Fatalf("разбор: %v", err)
|
||||
}
|
||||
if len(res.Uncovered) != 32 {
|
||||
t.Errorf("имён %d, ожидалось 32", len(res.Uncovered))
|
||||
}
|
||||
if res.UncoveredDropped != 8 {
|
||||
t.Errorf("отброшено %d, ожидалось 8 — иначе «ровно 32» неотличимо от «пришло пятьсот»", res.UncoveredDropped)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("длинное имя обрезано и помечено", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
long := strings.Repeat("щ", 60) // 120 байт
|
||||
res, err := hae.Parse([]byte(`{"data":{"`+long+`":[]}}`), hae.Meta{})
|
||||
if err != nil {
|
||||
t.Fatalf("разбор: %v", err)
|
||||
}
|
||||
if len(res.Uncovered) != 1 {
|
||||
t.Fatalf("непокрытые %v", res.Uncovered)
|
||||
}
|
||||
got := res.Uncovered[0]
|
||||
if !strings.HasSuffix(got, "…") {
|
||||
t.Errorf("имя %q без маркера обрезки: обрезка не инъективна и обязана быть видимой", got)
|
||||
}
|
||||
if !utf8.ValidString(got) {
|
||||
t.Errorf("имя %q обрезано посреди руны", got)
|
||||
}
|
||||
})
|
||||
|
||||
// Ошибка после разобранной секции metrics: правило прежнее — при ошибке
|
||||
// точек нет вовсе, иначе часть точек оказалась бы в витрине под статусом,
|
||||
// по которому доставку никто не подберёт.
|
||||
t.Run("обрыв после метрик точек не отдаёт", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const body = `{"data":{"metrics":[{"name":"a","data":[{"date":"2025-06-05 10:00:00 +0300","qty":1}]}],"work`
|
||||
res, err := hae.Parse([]byte(body), hae.Meta{})
|
||||
if !errors.Is(err, hae.ErrMalformed) {
|
||||
t.Fatalf("ошибка %v, ожидалась ErrMalformed", err)
|
||||
}
|
||||
if len(res.Points) != 0 {
|
||||
t.Errorf("точек %d, ожидалось 0", len(res.Points))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user