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) } } }