Files
healthlog/internal/points/points_internal_test.go
T
av 29ca8d415c httpapi: точки метрики за период отдаются одним запросом
- `GET /api/v1/metrics/{name}?from&to&layer` — ряд точек за период; конверт
  объявляет слой, измеренный род, его применимость к отданному ряду и границу
  окна измерения, а сам ряд собирается из одного слоя, выбранного по охвату
  точек внутри периода
- use-case вынесен в `internal/points`, чтение — одним входом `store.ReadSeries`
  под одной транзакцией; правило выбора слоя остаётся в домене и приходит в
  хранилище колбэком
- `writeJSON` перестал экранировать HTML-символы и перестал глушить отказ
  записи: дословность содержимого точки иначе не удерживается, а оборванное
  тело уходило под видом успешного `200`
2026-08-04 18:46:45 +03:00

133 lines
4.8 KiB
Go

package points
import (
"testing"
"time"
"git.vakhrushev.me/av/healthlog/internal/catalog"
"git.vakhrushev.me/av/healthlog/internal/store"
)
func day(d int) time.Time { return time.Date(2026, 6, d, 0, 0, 0, 0, time.UTC) }
func span(layer string, first, last time.Time) store.LayerSpan {
return store.LayerSpan{Layer: layer, First: first, Last: last}
}
// Правило выбора слоя целиком: охват, равенство охватов, выбывание по пустому
// пересечению и вырожденный вход.
func TestВыборСлояПоОхвату(t *testing.T) {
from, to := day(1), day(30)
cases := []struct {
name string
spans []store.LayerSpan
want string
}{
{
// «Вес за год»: нижний слой плотнее, но короче. Победа по числу
// точек вернула бы три дня вместо периода и не сказала бы ни слова.
name: "мелкий слой охватывает меньше крупного",
spans: []store.LayerSpan{
span("raw", day(1), day(4)),
span("hour", day(1), day(29)),
},
want: "hour",
},
{
name: "охваты равны — побеждает мелкий",
spans: []store.LayerSpan{
span("hour", day(1), day(29)),
span("minute", day(1), day(29)),
},
want: "minute",
},
{
// Слой, чьи данные лежат целиком вне периода, выбывает — иначе он
// выиграл бы охват и отдал пустой ряд.
name: "слой лежит вне периода",
spans: []store.LayerSpan{
span("hour", day(1).Add(-48*time.Hour), day(1).Add(-24*time.Hour)),
span("minute", day(2), day(3)),
},
want: "minute",
},
{
// Пересечение считается по ПЕРИОДУ, а не по данным: слой, торчащий
// за обе границы, не получает бесконечного охвата.
name: "слой шире периода",
spans: []store.LayerSpan{
span("hour", day(1).Add(-240*time.Hour), day(30).Add(240*time.Hour)),
span("minute", day(1), day(30)),
},
want: "minute",
},
{
name: "слоёв нет вовсе",
spans: nil,
want: "",
},
{
// Единственная точка ровно на границе периода: охват нулевой, но
// слой пригоден — точка в ответ попадёт.
name: "единственная точка на границе",
spans: []store.LayerSpan{span("raw", from, from)},
want: "raw",
},
{
// Точка ровно на `to` в период не входит (полуинтервал), значит и
// слой из выбора выбывает.
name: "единственная точка на правой границе",
spans: []store.LayerSpan{span("raw", to, to)},
want: "",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if got := pickLayer(from, to, c.spans); got != c.want {
t.Errorf("выбран слой %q, ожидался %q", got, c.want)
}
})
}
}
// Порядок слоёв в выборке не должен влиять на исход: иначе ответ стал бы
// функцией порядка строк, который задаёт SQLite, а не правило.
func TestВыборСлояНеЗависитОтПорядка(t *testing.T) {
from, to := day(1), day(30)
a := span("hour", day(1), day(29))
b := span("minute", day(1), day(29))
if pickLayer(from, to, []store.LayerSpan{a, b}) != pickLayer(from, to, []store.LayerSpan{b, a}) {
t.Error("выбор слоя зависит от порядка охватов")
}
}
// Применимость рода к ОТДАННОМУ ряду. Разряд, ради которого поле заведено:
// накопительная метрика на нижнем слое HAE неприменима, потому что нижний слой
// это интерполяция, а не сэмплы, и сумма по нему завышает втрое.
func TestПрименимостьРодаКРяду(t *testing.T) {
cases := []struct {
style catalog.Style
layer string
want bool
}{
{catalog.Cumulative, "raw", false},
{catalog.Cumulative, "minute", true},
{catalog.Cumulative, "hour", true},
{catalog.Cumulative, "sample", true},
{catalog.Instant, "raw", true},
{catalog.Unknown, "minute", false},
{catalog.Unknown, "raw", false},
{catalog.Cumulative, "", false},
{catalog.Instant, "", false},
}
for _, c := range cases {
if got := applicable(c.style, c.layer); got != c.want {
t.Errorf("применимость %s на слое %q = %v, ожидалось %v", c.style, c.layer, got, c.want)
}
}
}