- свёртка спрашивает журнал, встречалось ли имя строго раньше по паре (received_at, id), и пишет WARN с атрибутом uncovered_new; повторные молчат. Признак выводится, а не хранится — реестр был бы второй копией факта - добавлена подкоманда `healthlog uncovered`: перечень накопленного, чтение только на чтение, экранированные имена и названные границы носителя - синк документации: ADR о выводе новизны из журнала, две записи в журнал дефектов, два правила промоутом в конвенции, терминал оператора назван адресатом недоверенного входа
139 lines
5.5 KiB
Go
139 lines
5.5 KiB
Go
package fold
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"log/slog"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.vakhrushev.me/av/healthlog/internal/store"
|
|
)
|
|
|
|
// Внутренний тест, а не через `Fold`, и это осознанно. Сломать сверку снаружи
|
|
// больше нечем: испорченную учётную запись запрос теперь пропускает, а
|
|
// единственная настоящая причина отказа — отмена контекста и занятость базы —
|
|
// в полном проходе свёртки утащила бы за собой и слияние, то есть проверялась
|
|
// бы уже другая ветвь. Заводить интерфейс хранилища ради мока дороже: он
|
|
// пережил бы тест и остался бы в коде навсегда.
|
|
func TestNoveltyОтменённыйКонтекстНеМолчит(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
st, err := store.Open(filepath.Join(dir, "healthlog.db"))
|
|
if err != nil {
|
|
t.Fatalf("база: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = st.Close() })
|
|
|
|
s := &Service{store: st, log: slog.New(slog.DiscardHandler)}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
got := s.novelty(ctx, []string{"ecg", "symptoms"}, store.DeliveryRef{
|
|
ID: "d1",
|
|
ReceivedAt: time.Now().UTC(),
|
|
})
|
|
|
|
if !got.unknown {
|
|
t.Fatal("сверка на отменённом контексте объявила себя состоявшейся")
|
|
}
|
|
if len(got.fresh) != 2 {
|
|
t.Errorf("новыми объявлены %v, ожидались все имена доставки", got.fresh)
|
|
}
|
|
if got.cause == nil {
|
|
t.Error("причина отказа потеряна: занятость базы и вечную порчу колонки по признаку не различить")
|
|
}
|
|
}
|
|
|
|
// Пустой список сверку не берёт вовсе: на живом потоке все три приезжающие
|
|
// секции покрыты, и запрос по журналу в штатном режиме не стоит ничего.
|
|
func TestNoveltyПустойСписокЗапросаНеБерёт(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := &Service{store: nil, log: slog.New(slog.DiscardHandler)}
|
|
|
|
// Хранилище nil: возьмись запрос — тест упал бы паникой.
|
|
got := s.novelty(context.Background(), nil, store.DeliveryRef{ID: "d1"})
|
|
if got.unknown || len(got.fresh) != 0 {
|
|
t.Errorf("на пустом списке сверка что-то решила: %+v", got)
|
|
}
|
|
}
|
|
|
|
// Признак несостоявшейся сверки без причины неразбираем: занятость базы пройдёт
|
|
// сама, испорченная колонка не пройдёт никогда и будет поднимать признак на
|
|
// каждой доставке.
|
|
func TestLogResultПечатаетПричинуНесостоявшейсяСверки(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var buf bytes.Buffer
|
|
s := &Service{log: slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo}))}
|
|
|
|
s.logResult(context.Background(), "d1", Stats{
|
|
Uncovered: []string{"ecg"},
|
|
UncoveredNew: []string{"ecg"},
|
|
UncoveredSeenUnknown: true,
|
|
UncoveredSeenError: errors.New("журнал недоступен"),
|
|
})
|
|
|
|
var rec map[string]any
|
|
line := strings.TrimSpace(buf.String())
|
|
if err := json.Unmarshal([]byte(line), &rec); err != nil {
|
|
t.Fatalf("запись не разбирается: %v", err)
|
|
}
|
|
if rec["uncovered_seen_unknown"] != true {
|
|
t.Error("признак несостоявшейся сверки не выставлен")
|
|
}
|
|
if rec["uncovered_seen_error"] != "журнал недоступен" {
|
|
t.Errorf("причина в записи: %v", rec["uncovered_seen_error"])
|
|
}
|
|
if rec["level"] != "WARN" {
|
|
t.Errorf("уровень %v, ожидался WARN: имена объявлены новыми", rec["level"])
|
|
}
|
|
}
|
|
|
|
// Запись об отказе несёт и признак несостоявшейся сверки, и причину: у отказа
|
|
// разбора та же цена молчания, что у успешного пути — имя уже в учёте.
|
|
func TestFailПечатаетНесостоявшуюсяСверку(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dir := t.TempDir()
|
|
st, err := store.Open(filepath.Join(dir, "healthlog.db"))
|
|
if err != nil {
|
|
t.Fatalf("база: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = st.Close() })
|
|
|
|
ctx := context.Background()
|
|
err = st.CreateDelivery(ctx, store.Delivery{
|
|
ID: "d1", ReceivedAt: time.Now().UTC(), RawPath: "d1.json.gz",
|
|
SHA256: "-", ParseStatus: store.ParsePending,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("учёт доставки: %v", err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
s := &Service{
|
|
store: st,
|
|
log: slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo})),
|
|
}
|
|
|
|
s.fail(ctx, "d1", errors.New("непонятое содержимое"), parseResidue{
|
|
uncovered: []string{"ecg"},
|
|
novelty: sectionNovelty{fresh: []string{"ecg"}, unknown: true, cause: errors.New("журнал недоступен")},
|
|
})
|
|
|
|
out := buf.String()
|
|
for _, want := range []string{"uncovered_new", "ecg", "uncovered_seen_unknown", "журнал недоступен"} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("в записи об отказе нет %q:\n%s", want, out)
|
|
}
|
|
}
|
|
}
|