Files
jschoubben 954728b390
mesh/repo-check head 954728b39042 merged into trunk 8ae804fe6450 beside mesh-controller#388@ac79e293 — pass: its merge-check.sh passed
mesh/merge-gate head 954728b39042 merged into trunk 8ae804fe6450 — pass: group feat/delivery-watch composed: every machine composes with the change as it d…
mesh/delivery delivered
mesh/delivery-group group feat/delivery-watch delivered: every member is delivered
Show what the watchman says: offline presence, stuck keys and unreadable values, in the view and its command (issue 1168)
2026-10-19 00:55:19 +02:00

154 lines
4.5 KiB
Go

package view
import (
"bufio"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
func served(t *testing.T) (string, *Model) {
t.Helper()
m := New()
fill(m, base(t)...)
fill(m, haveE(t, "a", "w", have(t, "b9", delivery.PhaseRunning, delivery.HealthOK, ""), t0))
dir, err := os.MkdirTemp(os.Getenv("TMPDIR"), "s")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { os.RemoveAll(dir) })
path := filepath.Join(dir, "run", "cli.sock")
l, err := ListenSocket(path)
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() { ServeSocket(ctx, l, m); close(done) }()
t.Cleanup(func() { cancel(); l.Close(); <-done })
return path, m
}
func TestSocketAnswersRows(t *testing.T) { // 1134/AC6
path, m := served(t)
got, err := Ask(path)
if err != nil {
t.Fatal(err)
}
want := m.Snapshot()
b1, _ := json.Marshal(got)
b2, _ := json.Marshal(want)
if len(want.Rows) == 0 || string(b1) != string(b2) {
t.Fatalf("socket answered %s, want %s", b1, b2)
}
st, err := os.Stat(path)
if err != nil || st.Mode().Perm() != 0o666 {
t.Fatalf("socket mode = %v, %v; want 0666", st.Mode().Perm(), err)
}
dst, _ := os.Stat(filepath.Dir(path))
if dst.Mode().Perm() != 0o755 {
t.Fatalf("directory mode = %v, want 0755", dst.Mode().Perm())
}
}
func TestSocketRefusesAnyOtherAsk(t *testing.T) { // 1134/AC6
path, m := served(t)
before, _ := json.Marshal(m.Snapshot())
for _, ask := range []string{`{"ask":"start","module":"x"}`, `{"ask":"retry"}`, `{"ask":""}`, `not json`, `{}`} {
c, err := dial(path)
if err != nil {
t.Fatal(err)
}
c.Write([]byte(ask + "\n"))
line, err := bufio.NewReader(c).ReadString('\n')
c.Close()
if err != nil || !strings.Contains(line, `"error"`) || strings.Contains(line, `"rows"`) {
t.Errorf("ask %q answered %q, %v; want a refusal", ask, line, err)
}
}
after, _ := json.Marshal(m.Snapshot())
if string(before) != string(after) {
t.Fatal("a refused ask changed the rows")
}
}
func TestTableShowsEveryRow(t *testing.T) { // 1134/AC6
_, m := served(t)
var out strings.Builder
s := m.Snapshot()
Table(&out, s)
for _, r := range s.Rows {
if !strings.Contains(out.String(), r.Module) {
t.Fatalf("table lacks %s:\n%s", r.Module, out.String())
}
}
for _, w := range []string{"NODE", "MODULE", "ASSIGNED", "HAVE", "PHASE", "HEALTH", "SINCE", "WAITS", "no have yet", "left behind"} {
if !strings.Contains(out.String(), w) {
t.Errorf("table lacks %q:\n%s", w, out.String())
}
}
}
func TestWebIsTheSameRowsAndReadOnly(t *testing.T) { // 1134/AC6
_, m := served(t)
h := Web(m)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
if rec.Code != 200 || !strings.Contains(rec.Body.String(), "no have yet") || !strings.Contains(rec.Header().Get("Content-Type"), "text/html") {
t.Fatalf("web = %d %q", rec.Code, rec.Body.String())
}
rec = httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/", strings.NewReader("x")))
if rec.Code != http.StatusMethodNotAllowed {
t.Fatalf("POST = %d, want 405", rec.Code)
}
}
func watchmanSnapshot(t *testing.T) *Model {
t.Helper()
m := New()
fill(m, presenceE(t, "a", false, t0), stuckE(t, "node-engine_have", "a.x", "a report from a", t0, false),
Entry{Bucket: delivery.PresenceBucket, Key: "c", Value: []byte(`{"v":9}`), At: t0})
return m
}
func TestTableAndWebShowTheWatchmansLists(t *testing.T) { // 1168/AC4
m := watchmanSnapshot(t)
var out strings.Builder
Table(&out, m.Snapshot())
rec := httptest.NewRecorder()
Web(m).ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
for name, text := range map[string]string{"table": out.String(), "web": rec.Body.String()} {
for _, w := range []string{"OFFLINE", "STUCK", "UNREADABLE", "node-engine_have", "a.x", "a report from a", "offline", "c"} {
if !strings.Contains(text, w) {
t.Errorf("%s lacks %q:\n%s", name, w, text)
}
}
}
}
func TestSocketCarriesTheWatchmansLists(t *testing.T) { // 1168/AC4
m := watchmanSnapshot(t)
dir, _ := os.MkdirTemp(os.Getenv("TMPDIR"), "w")
t.Cleanup(func() { os.RemoveAll(dir) })
path := filepath.Join(dir, "cli.sock")
l, err := ListenSocket(path)
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
go ServeSocket(ctx, l, m)
got, err := Ask(path)
if err != nil || len(got.Offline) != 1 || len(got.Stuck) != 1 || len(got.Unreadable) != 1 {
t.Fatalf("socket = %+v, %v", got, err)
}
}