Every check is also counted in a per-day table that outlives the raw checks, so the public page can draw one bar per UTC day with a tooltip, as most hosted status pages do. Existing databases are backfilled once from the checks they still have. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
89 lines
2.6 KiB
Go
89 lines
2.6 KiB
Go
package sandbar
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Every check lands in a per-day count that outlives the raw checks, and the status page
|
|
// gets one [checks, failed] pair per day, oldest first and ending today.
|
|
func TestUptimeBars(t *testing.T) {
|
|
s, clk, _ := newTestServer(t)
|
|
m := &Monitor{ID: "m", Name: "Site", Kind: "http", Target: "http://x", Enabled: true, Public: true, Retries: 1}
|
|
s.st.put("monitor", m.ID, m)
|
|
|
|
// Two days ago: 4 checks, 1 failed. Yesterday: nothing. Today: 2 checks, both fine.
|
|
clk.Advance(-48 * time.Hour)
|
|
for i := 0; i < 4; i++ {
|
|
var err error
|
|
if i == 3 {
|
|
err = errors.New("timeout")
|
|
}
|
|
s.recordResult(m, s.monitorState(m.ID), clk.Now(), 10*time.Millisecond, err)
|
|
}
|
|
clk.Advance(48 * time.Hour)
|
|
s.recordResult(m, s.monitorState(m.ID), clk.Now(), 10*time.Millisecond, nil)
|
|
s.recordResult(m, s.monitorState(m.ID), clk.Now(), 10*time.Millisecond, nil)
|
|
|
|
days := s.dailyUptime(m.ID, 5, clk.Now())
|
|
want := [][2]int{{0, 0}, {0, 0}, {4, 1}, {0, 0}, {2, 0}}
|
|
for i := range want {
|
|
if days[i] != want[i] {
|
|
t.Fatalf("days = %v, want %v", days, want)
|
|
}
|
|
}
|
|
|
|
// The rollup survives pruning the raw checks.
|
|
s.st.prune(clk.Now().AddDate(0, 0, 60), 30, 1)
|
|
if got := s.dailyUptime(m.ID, 5, clk.Now()); got[2] != [2]int{4, 1} {
|
|
t.Fatalf("rollup lost after pruning: %v", got)
|
|
}
|
|
|
|
ts := httptest.NewServer(s.Handler())
|
|
defer ts.Close()
|
|
resp, err := http.Get(ts.URL + "/status.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer resp.Body.Close()
|
|
var out struct {
|
|
Today string
|
|
Monitors []struct {
|
|
Uptime float64
|
|
Days [][2]int
|
|
}
|
|
}
|
|
json.NewDecoder(resp.Body).Decode(&out)
|
|
if out.Today != "2026-01-05" || len(out.Monitors) != 1 || len(out.Monitors[0].Days) != uptimeDays {
|
|
t.Fatalf("status.json: %+v", out)
|
|
}
|
|
m0 := out.Monitors[0]
|
|
if last := m0.Days[uptimeDays-1]; last != [2]int{2, 0} {
|
|
t.Fatalf("today's bar: %v", last)
|
|
}
|
|
if want := 100 * 5.0 / 6.0; m0.Uptime < want-0.01 || m0.Uptime > want+0.01 {
|
|
t.Fatalf("uptime %.2f, want %.2f", m0.Uptime, want)
|
|
}
|
|
}
|
|
|
|
// A database from before the daily table gets it built from the checks it has.
|
|
func TestDailyBackfill(t *testing.T) {
|
|
s, clk, _ := newTestServer(t)
|
|
now := clk.Now().Unix()
|
|
s.st.db.Exec(`INSERT INTO checks(monitor_id,time,ok,ms,detail) VALUES('m',?,1,5,''),('m',?,0,5,'x')`, now, now+1)
|
|
s.st.db.Exec(`DELETE FROM daily`)
|
|
var have int
|
|
s.st.db.QueryRow(`SELECT count(*) FROM daily`).Scan(&have)
|
|
if have != 0 {
|
|
t.Fatal("setup")
|
|
}
|
|
s.st.backfillDaily()
|
|
if got := s.dailyUptime("m", 1, clk.Now()); got[0] != [2]int{2, 1} {
|
|
t.Fatalf("backfill: %v", got)
|
|
}
|
|
}
|