228 lines
7.6 KiB
Go
228 lines
7.6 KiB
Go
package probe
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Span math (hub api::span): budget defaults to 1440 points, the
|
|
// caller's points may only lower it and is clamped to [60, 1440];
|
|
// windows past the 7-day detail window switch to whole-hour steps;
|
|
// since aligns down to the step.
|
|
func TestComputeSpan(t *testing.T) {
|
|
now := int64(1_800_000_000) // arbitrary fixed instant
|
|
i64 := func(v int64) *int64 { return &v }
|
|
|
|
cases := []struct {
|
|
name string
|
|
hours int64
|
|
points *int64
|
|
wantStep int64
|
|
wantHourly bool
|
|
}{
|
|
{"default 6h", 6, nil, 60, false},
|
|
{"points lower the budget", 6, i64(60), 360, false},
|
|
{"points above cap ignored", 6, i64(99999), 60, false},
|
|
{"points below floor clamped", 1, i64(1), 60, false},
|
|
{"detail edge stays minute", DetailDays * 24, nil, 420, false},
|
|
{"past detail reads hourly", DetailDays*24 + 1, nil, 3600, true},
|
|
{"30 days hourly", 720, nil, 3600, true},
|
|
{"hourly step grows past budget", 24 * 365, nil, 3600 * 7, true},
|
|
}
|
|
for _, tc := range cases {
|
|
sp := ComputeSpan(tc.hours, tc.points, now)
|
|
if sp.Step != tc.wantStep || sp.Hourly != tc.wantHourly {
|
|
t.Errorf("%s: got step=%d hourly=%v, want step=%d hourly=%v",
|
|
tc.name, sp.Step, sp.Hourly, tc.wantStep, tc.wantHourly)
|
|
}
|
|
if sp.Since%sp.Step != 0 {
|
|
t.Errorf("%s: since %d not aligned to step %d", tc.name, sp.Since, sp.Step)
|
|
}
|
|
if want := now - tc.hours*3600; sp.Since > want || sp.Since <= want-sp.Step {
|
|
t.Errorf("%s: since %d not the aligned window start of %d", tc.name, sp.Since, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func insertMinute(t *testing.T, st *Store, agent, ts int64, cpu float64, memUsed, netRx, netRxMax int64, cpuMax float64, tcp int64) {
|
|
t.Helper()
|
|
_, err := st.db.Exec(`INSERT INTO probe_metric
|
|
(agent_id, ts, cpu, mem_used, swap_used, disk_used, net_rx, net_tx,
|
|
net_rx_max, net_tx_max, tcp, udp, procs, cpu_max)
|
|
VALUES (?, ?, ?, ?, 0, 0, ?, 0, ?, 0, ?, 0, 0, ?)`,
|
|
agent, ts, cpu, memUsed, netRx, netRxMax, tcp, cpuMax)
|
|
if err != nil {
|
|
t.Fatalf("insert minute: %v", err)
|
|
}
|
|
}
|
|
|
|
// Minute windows fold rows per bucket with the hub's asymmetric
|
|
// aggregates: byte columns truncated means, count columns rounded,
|
|
// peaks take the max of value and stored peak.
|
|
func TestMetricHistoryMinuteFold(t *testing.T) {
|
|
st := newTestStore(t)
|
|
now := time.Now().Unix()
|
|
base := now - now%60 - 120 // a minute boundary two minutes back
|
|
|
|
insertMinute(t, st, 7, base, 10, 100, 1000, 1500, 12, 3)
|
|
insertMinute(t, st, 7, base+60, 20, 201, 2000, 500, 25, 5)
|
|
|
|
// 1:1 buckets (step 60).
|
|
sp := ComputeSpan(1, nil, now)
|
|
rows, err := st.MetricHistory(7, sp)
|
|
if err != nil {
|
|
t.Fatalf("history: %v", err)
|
|
}
|
|
if len(rows) != 2 {
|
|
t.Fatalf("want 2 points, got %d (%+v)", len(rows), rows)
|
|
}
|
|
if rows[0].Ts != base || rows[0].Cpu != 10 || rows[0].MemUsed != 100 ||
|
|
rows[0].NetRxMax != 1500 || rows[0].CpuMax != 12 || rows[0].Minutes != 1 || rows[0].Tcp != 3 {
|
|
t.Fatalf("point 0 wrong: %+v", rows[0])
|
|
}
|
|
if rows[1].Ts != base+60 || rows[1].NetRxMax != 2000 { // max(2000, 500)
|
|
t.Fatalf("point 1 wrong: %+v", rows[1])
|
|
}
|
|
|
|
// Both minutes in one 120 s bucket: cpu mean 15, mem truncated
|
|
// mean (100+201)/2 = 150, peaks survive, minutes counts rows.
|
|
sp2 := Span{Since: base, Step: 120, Hourly: false}
|
|
rows, err = st.MetricHistory(7, sp2)
|
|
if err != nil {
|
|
t.Fatalf("history 120s: %v", err)
|
|
}
|
|
if len(rows) != 1 {
|
|
t.Fatalf("want 1 folded point, got %d", len(rows))
|
|
}
|
|
r := rows[0]
|
|
if r.Ts != base || r.Cpu != 15 || r.MemUsed != 150 || r.Minutes != 2 ||
|
|
r.NetRxMax != 2000 || r.CpuMax != 25 || r.Tcp != 4 { // ROUND(avg(3,5)) = 4
|
|
t.Fatalf("folded point wrong: %+v", r)
|
|
}
|
|
|
|
// Another agent sees nothing.
|
|
rows, err = st.MetricHistory(8, sp)
|
|
if err != nil || len(rows) != 0 {
|
|
t.Fatalf("agent isolation: rows=%v err=%v", rows, err)
|
|
}
|
|
}
|
|
|
|
// Hourly windows merge the folded hour tier (weighted by each row's
|
|
// minute count) with the minute rows not yet folded.
|
|
func TestMetricHistoryHourlyFold(t *testing.T) {
|
|
st := newTestStore(t)
|
|
now := time.Now().Unix()
|
|
hour := now - now%3600
|
|
h1 := hour - 100*3600 // folded tier
|
|
h2 := h1 + 3600
|
|
|
|
ins := func(ts int64, cpu float64, mem int64, minutes int64) {
|
|
_, err := st.db.Exec(`INSERT INTO probe_metric_hour
|
|
(agent_id, ts, cpu, mem_used, swap_used, disk_used, net_rx, net_tx,
|
|
net_rx_max, net_tx_max, tcp, udp, procs, cpu_max, minutes)
|
|
VALUES (?, ?, ?, ?, 0, 0, 0, 0, 0, 0, 0, 0, 0, ?, ?)`,
|
|
9, ts, cpu, mem, cpu, minutes)
|
|
if err != nil {
|
|
t.Fatalf("insert hour: %v", err)
|
|
}
|
|
}
|
|
ins(h1, 30, 300, 60)
|
|
ins(h2, 60, 600, 30)
|
|
if _, err := st.db.Exec(`INSERT INTO probe_setting (key, value) VALUES (?, ?)`,
|
|
rolledKey, h2+3600); err != nil {
|
|
t.Fatalf("set watermark: %v", err)
|
|
}
|
|
// One unfolded minute row in the current hour.
|
|
insertMinute(t, st, 9, hour+60, 90, 900, 5000, 5000, 90, 9)
|
|
|
|
sp := ComputeSpan(200, nil, now)
|
|
if !sp.Hourly || sp.Step != 3600 {
|
|
t.Fatalf("span: %+v", sp)
|
|
}
|
|
rows, err := st.MetricHistory(9, sp)
|
|
if err != nil {
|
|
t.Fatalf("history: %v", err)
|
|
}
|
|
if len(rows) != 3 {
|
|
t.Fatalf("want 3 points (2 folded + 1 live), got %d (%+v)", len(rows), rows)
|
|
}
|
|
if rows[0].Ts != h1 || rows[0].Cpu != 30 || rows[0].Minutes != 60 || rows[0].MemUsed != 300 {
|
|
t.Fatalf("folded point 0 wrong: %+v", rows[0])
|
|
}
|
|
if rows[1].Ts != h2 || rows[1].Cpu != 60 || rows[1].Minutes != 30 {
|
|
t.Fatalf("folded point 1 wrong: %+v", rows[1])
|
|
}
|
|
if rows[2].Ts != hour || rows[2].Cpu != 90 || rows[2].Minutes != 1 || rows[2].NetRx != 5000 {
|
|
t.Fatalf("live point wrong: %+v", rows[2])
|
|
}
|
|
}
|
|
|
|
// Meta lifecycle: defaults for unknown agents, full round-trip,
|
|
// replace-on-upsert, and the delete cascade emptying every tier.
|
|
func TestMetaCRUDAndDelete(t *testing.T) {
|
|
st := newTestStore(t)
|
|
|
|
if _, ok, err := st.Meta(42); err != nil || ok {
|
|
t.Fatalf("unknown agent: ok=%v err=%v", ok, err)
|
|
}
|
|
d := DefaultMeta(42)
|
|
if d.Currency != "USD" || d.BillingCycle != "monthly" || !d.Public ||
|
|
d.TrafficMode != "sum" || d.TrafficResetDay != 1 || d.Notify {
|
|
t.Fatalf("defaults drifted from the DDL: %+v", d)
|
|
}
|
|
|
|
exp := "2030-01-02"
|
|
want := Meta{
|
|
AgentID: 42, Price: 12.5, Currency: "USD", BillingCycle: "yearly",
|
|
ExpiresAt: &exp, TrafficLimit: 1_000_000_000, TrafficMode: "max",
|
|
TrafficResetDay: 15, Public: false, Sort: 3, GroupName: "g1",
|
|
PublicRemark: "hello", Notify: true, CountryPin: "JP",
|
|
IPv4Pin: "203.0.113.7", IPv6Pin: "2001:db8::7",
|
|
}
|
|
if err := st.UpsertMeta(want); err != nil {
|
|
t.Fatalf("upsert: %v", err)
|
|
}
|
|
got, ok, err := st.Meta(42)
|
|
if err != nil || !ok {
|
|
t.Fatalf("read back: ok=%v err=%v", ok, err)
|
|
}
|
|
gotExp, wantExp := "", ""
|
|
if got.ExpiresAt != nil {
|
|
gotExp = *got.ExpiresAt
|
|
}
|
|
if want.ExpiresAt != nil {
|
|
wantExp = *want.ExpiresAt
|
|
}
|
|
got.ExpiresAt, want.ExpiresAt = nil, nil
|
|
if got != want || gotExp != wantExp {
|
|
t.Fatalf("round trip mismatch:\n got %+v exp=%q\nwant %+v exp=%q", got, gotExp, want, wantExp)
|
|
}
|
|
want.ExpiresAt = &exp
|
|
|
|
want.Price = 99
|
|
if err := st.UpsertMeta(want); err != nil {
|
|
t.Fatalf("replace: %v", err)
|
|
}
|
|
all, err := st.AllMeta()
|
|
if err != nil || len(all) != 1 || all[42].Price != 99 {
|
|
t.Fatalf("all meta: %v %+v", err, all)
|
|
}
|
|
|
|
insertMinute(t, st, 42, 1000, 1, 1, 1, 1, 1, 1)
|
|
if _, err := st.db.Exec(`INSERT INTO probe_traffic (agent_id) VALUES (42)`); err != nil {
|
|
t.Fatalf("seed traffic: %v", err)
|
|
}
|
|
if err := st.DeleteAgentData(42); err != nil {
|
|
t.Fatalf("delete: %v", err)
|
|
}
|
|
if _, ok, _ := st.Meta(42); ok {
|
|
t.Fatal("meta survived delete")
|
|
}
|
|
for _, table := range []string{"probe_metric", "probe_traffic"} {
|
|
var n int
|
|
if err := st.db.QueryRow(`SELECT COUNT(*) FROM ` + table + ` WHERE agent_id = 42`).Scan(&n); err != nil || n != 0 {
|
|
t.Fatalf("%s survived delete: n=%d err=%v", table, n, err)
|
|
}
|
|
}
|
|
}
|