первая встреча непокрытой секции стала наблюдаемым событием
- свёртка спрашивает журнал, встречалось ли имя строго раньше по паре (received_at, id), и пишет WARN с атрибутом uncovered_new; повторные молчат. Признак выводится, а не хранится — реестр был бы второй копией факта - добавлена подкоманда `healthlog uncovered`: перечень накопленного, чтение только на чтение, экранированные имена и названные границы носителя - синк документации: ADR о выводе новизны из журнала, две записи в журнал дефектов, два правила промоутом в конвенции, терминал оператора назван адресатом недоверенного входа
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user