Каталог разрезов и измеренный род агрегации
- род метрики выводится сверкой минутного слоя с часовым: часовое значение сходится с суммой минутных — накопительная, со средним — мгновенная, иначе `unknown` и свёртка не предлагается вовсе. На живом архиве (123 доставки, 31 метрика) 7 накопительных, 9 мгновенных, противоречащих часов ноль - `GET /api/v1/metrics` под токеном чтения отдаёт единицы, слои с границами и род вместе с основанием измерения; род нигде не хранится — он функция витрины, а витрина функция журнала, устаревать в нём нечему - миграция 00009: покрывающий индекс, чтобы каталог отвечал по учётным колонкам, не разжимая содержимое объектов
This commit is contained in:
@@ -6,12 +6,16 @@ import (
|
||||
"context"
|
||||
"flag"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.vakhrushev.me/av/healthlog/internal/catalog"
|
||||
"git.vakhrushev.me/av/healthlog/internal/hae"
|
||||
"git.vakhrushev.me/av/healthlog/internal/ident"
|
||||
"git.vakhrushev.me/av/healthlog/internal/store"
|
||||
)
|
||||
@@ -132,6 +136,8 @@ func TestReplayЖивогоАрхива(t *testing.T) {
|
||||
// Значений точек отпечаток не раскрывает: содержимое входит в него хешем.
|
||||
t.Logf("объектов %d, отпечаток содержимого %s", first.Buckets, first.Fingerprint)
|
||||
|
||||
measureStyles(t, dst)
|
||||
|
||||
// Главное свойство ключа: у записей сна он ИНТЕРВАЛ, а не метка — под одним
|
||||
// `date` лежит до трёх записей (docs/local-research.md, находка 47).
|
||||
//
|
||||
@@ -151,6 +157,72 @@ func TestReplayЖивогоАрхива(t *testing.T) {
|
||||
t.Logf("координат сна %d, различных меток %d", coords, labels)
|
||||
}
|
||||
|
||||
// measureStyles прогоняет измерение рода агрегации на витрине, собранной из
|
||||
// живого архива, — единственное место, где правило проверяется на настоящем
|
||||
// потоке, а не на фикстурах.
|
||||
//
|
||||
// Утверждаются СВОЙСТВА, а не числа: корпус растёт с каждой доставкой, а прогон
|
||||
// живого архива в гейт не входит, так что константа, производная от размера
|
||||
// корпуса, покраснела бы молча (docs/review-journal.md, 2026-08-02). Измеренные
|
||||
// числа печатаются.
|
||||
func measureStyles(t *testing.T, st *store.Store) {
|
||||
t.Helper()
|
||||
|
||||
started := time.Now()
|
||||
metrics, err := catalog.New(st, slog.New(slog.DiscardHandler)).Metrics(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("каталог: %v", err)
|
||||
}
|
||||
elapsed := time.Since(started)
|
||||
|
||||
byStyle := map[catalog.Style]int{}
|
||||
for _, m := range metrics {
|
||||
byStyle[m.Aggregation.Style]++
|
||||
|
||||
// Главное свойство правила: свидетельства единогласны. Противоречие —
|
||||
// событие для разбора, а не отказ прогона, поэтому оно печатается с
|
||||
// координатами и валит тест: пока его нет, посылка «род измерим» верна.
|
||||
if m.Aggregation.Conflicting > 0 {
|
||||
t.Errorf("%s: противоречащих часов %d при %d согласных — свидетельства разошлись",
|
||||
m.Metric, m.Aggregation.Conflicting, m.Aggregation.Agreeing)
|
||||
}
|
||||
|
||||
// Род измеряется только сверкой минутного слоя с часовым. Метрика с
|
||||
// объявленным родом обязана иметь оба слоя: иначе он выведен из
|
||||
// нижнего, а нижний слой HAE — посекундная развёртка, и его сумма
|
||||
// завышена.
|
||||
if m.Aggregation.Style == catalog.Unknown {
|
||||
continue
|
||||
}
|
||||
var hasMinute, hasHour bool
|
||||
for _, l := range m.Layers {
|
||||
hasMinute = hasMinute || l.Layer == string(hae.LayerMinute)
|
||||
hasHour = hasHour || l.Layer == string(hae.LayerHour)
|
||||
}
|
||||
if !hasMinute || !hasHour {
|
||||
t.Errorf("%s: род %v при слоях %+v — измерять было нечем", m.Metric, m.Aggregation.Style, m.Layers)
|
||||
}
|
||||
}
|
||||
|
||||
// Роды разошлись: правило различает накопительные и мгновенные, а не
|
||||
// сваливает всё в одну кучу и не отвечает `unknown` на весь корпус.
|
||||
if byStyle[catalog.Cumulative] == 0 {
|
||||
t.Error("накопительных метрик не нашлось — правило не различает роды")
|
||||
}
|
||||
if byStyle[catalog.Instant] == 0 {
|
||||
t.Error("мгновенных метрик не нашлось — правило не различает роды")
|
||||
}
|
||||
|
||||
t.Logf("каталог: метрик %d за %v; накопительных %d, мгновенных %d, неизвестных %d",
|
||||
len(metrics), elapsed.Round(time.Millisecond),
|
||||
byStyle[catalog.Cumulative], byStyle[catalog.Instant], byStyle[catalog.Unknown])
|
||||
for _, m := range metrics {
|
||||
t.Logf(" %-38s %-10s часов %d, пригодных %d, согласных %d",
|
||||
m.Metric, m.Aggregation.Style, m.Aggregation.Hours,
|
||||
m.Aggregation.Compared, m.Aggregation.Agreeing)
|
||||
}
|
||||
}
|
||||
|
||||
// countSleepKeys возвращает число различных координат записей сна и число
|
||||
// различных меток начала. Разница между ними и есть то, что теряет ключ по
|
||||
// метке.
|
||||
|
||||
Reference in New Issue
Block a user