mirror of
https://github.com/hanzoai/dashboards.git
synced 2026-10-30 20:19:37 +00:00
555 lines
20 KiB
Go
555 lines
20 KiB
Go
package server_test
|
|
|
|
import (
|
|
"context"
|
|
"math"
|
|
"testing"
|
|
"time"
|
|
|
|
basetests "github.com/hanzoai/base/tests"
|
|
luxlog "github.com/luxfi/log"
|
|
zaplib "github.com/luxfi/zap"
|
|
zcap "github.com/zap-proto/go/cap"
|
|
|
|
gen "github.com/hanzoai/dashboards/gen"
|
|
"github.com/hanzoai/dashboards/server"
|
|
)
|
|
|
|
const testProject = "proj-test"
|
|
|
|
// portSeq hands out unique ports per sub-service so parallel-ish tests on the
|
|
// same machine never collide on a listener. Base port is high + uncommon.
|
|
var portSeq = 19700
|
|
|
|
func nextPort() int { portSeq++; return portSeq }
|
|
|
|
// newService spins up a Base test app with the five collections provisioned, a
|
|
// ZAP router node listening, and returns the listen address, node id, cleanup.
|
|
func newService(t *testing.T) (addr, peerID string, cleanup func()) {
|
|
t.Helper()
|
|
app, err := basetests.NewTestApp()
|
|
if err != nil {
|
|
t.Fatalf("new test app: %v", err)
|
|
}
|
|
if err := server.EnsureCollections(app); err != nil {
|
|
t.Fatalf("ensure collections: %v", err)
|
|
}
|
|
port := nextPort()
|
|
id := "dash-test-srv-" + itoa(port)
|
|
node := zaplib.NewNode(zaplib.NodeConfig{NodeID: id, Port: port, NoDiscovery: true})
|
|
srv := server.NewServer(app, luxlog.New("component", "dash-test"), zcap.Verifier{})
|
|
srv.Register(node)
|
|
if err := node.Start(); err != nil {
|
|
t.Fatalf("node start: %v", err)
|
|
}
|
|
return "127.0.0.1:" + itoa(port), id, func() {
|
|
node.Stop()
|
|
app.Cleanup()
|
|
}
|
|
}
|
|
|
|
// newClient dials the service with a synthetic CapKindIAMSession cap holding the
|
|
// given permissions.
|
|
func newClient(t *testing.T, addr, peerID string, perms uint64) (*server.Client, func()) {
|
|
t.Helper()
|
|
capBuf, err := server.SyntheticCap(perms)
|
|
if err != nil {
|
|
t.Fatalf("synthetic cap: %v", err)
|
|
}
|
|
port := nextPort()
|
|
cli := zaplib.NewNode(zaplib.NodeConfig{NodeID: "dash-test-cli-" + itoa(port), Port: port, NoDiscovery: true})
|
|
if err := cli.Start(); err != nil {
|
|
t.Fatalf("client node start: %v", err)
|
|
}
|
|
c, err := server.Dial(cli, addr, peerID, capBuf)
|
|
if err != nil {
|
|
t.Fatalf("dial: %v", err)
|
|
}
|
|
time.Sleep(150 * time.Millisecond) // handshake settle
|
|
return c, func() { cli.Stop() }
|
|
}
|
|
|
|
// fullClient is a fully-authorized client (all DashPermissions bits).
|
|
func fullClient(t *testing.T, addr, peerID string) (*server.Client, func()) {
|
|
return newClient(t, addr, peerID, server.AllDashPermissions)
|
|
}
|
|
|
|
func ctx5(t *testing.T) (context.Context, func()) {
|
|
return context.WithTimeout(context.Background(), 5*time.Second)
|
|
}
|
|
|
|
// ─────────────────────────────── dashboardRouter ───────────────────────────
|
|
|
|
func TestDashboardCRUD(t *testing.T) {
|
|
addr, peer, stop := newService(t)
|
|
defer stop()
|
|
c, stopC := fullClient(t, addr, peer)
|
|
defer stopC()
|
|
ctx, cancel := ctx5(t)
|
|
defer cancel()
|
|
|
|
// Create
|
|
d, err := c.CreateDashboard(ctx, gen.CreateDashReqInput{ProjectId: testProject, Name: "Ops", Description: "ops board"})
|
|
if err != nil {
|
|
t.Fatalf("CreateDashboard: %v", err)
|
|
}
|
|
if d.Id() == "" || d.Name() != "Ops" || d.Owner() != "PROJECT" {
|
|
t.Fatalf("unexpected created dashboard: id=%q name=%q owner=%q", d.Id(), d.Name(), d.Owner())
|
|
}
|
|
|
|
// Get
|
|
got, err := c.GetDashboard(ctx, gen.IdReqInput{ProjectId: testProject, Id: d.Id()})
|
|
if err != nil {
|
|
t.Fatalf("GetDashboard: %v", err)
|
|
}
|
|
if got.Name() != "Ops" {
|
|
t.Fatalf("GetDashboard name = %q, want Ops", got.Name())
|
|
}
|
|
|
|
// Update metadata
|
|
if _, err := c.UpdateDashboardMetadata(ctx, gen.UpdateDashReqInput{
|
|
ProjectId: testProject, DashboardId: d.Id(), Name: "Ops v2", Description: "renamed",
|
|
}); err != nil {
|
|
t.Fatalf("UpdateDashboardMetadata: %v", err)
|
|
}
|
|
|
|
// Update definition + filters (JSON blobs round-trip verbatim)
|
|
const def = `{"widgets":[{"type":"widget","id":"w1","widgetId":"x","x":0,"y":0,"x_size":3,"y_size":3}]}`
|
|
dd, err := c.UpdateDashboardDefinition(ctx, gen.DashDefReqInput{ProjectId: testProject, DashboardId: d.Id(), Definition: def})
|
|
if err != nil {
|
|
t.Fatalf("UpdateDashboardDefinition: %v", err)
|
|
}
|
|
if dd.Definition() != def {
|
|
t.Fatalf("definition not round-tripped:\n got %q\nwant %q", dd.Definition(), def)
|
|
}
|
|
const filt = `[{"column":"name","operator":"=","value":"x","type":"string"}]`
|
|
df, err := c.UpdateDashboardFilters(ctx, gen.DashFiltersReqInput{ProjectId: testProject, DashboardId: d.Id(), Filters: filt})
|
|
if err != nil {
|
|
t.Fatalf("UpdateDashboardFilters: %v", err)
|
|
}
|
|
if df.Filters() != filt {
|
|
t.Fatalf("filters not round-tripped: got %q", df.Filters())
|
|
}
|
|
|
|
// Clone
|
|
cl, err := c.CloneDashboard(ctx, gen.IdReqInput{ProjectId: testProject, Id: d.Id()})
|
|
if err != nil {
|
|
t.Fatalf("CloneDashboard: %v", err)
|
|
}
|
|
if cl.Name() != "Ops v2 (Clone)" {
|
|
t.Fatalf("clone name = %q, want 'Ops v2 (Clone)'", cl.Name())
|
|
}
|
|
if cl.Definition() != def {
|
|
t.Fatalf("clone did not copy definition")
|
|
}
|
|
|
|
// List: original + clone = 2
|
|
list, err := c.AllDashboards(ctx, gen.ListReqInput{ProjectId: testProject, Page: 1, Limit: 50})
|
|
if err != nil {
|
|
t.Fatalf("AllDashboards: %v", err)
|
|
}
|
|
if list.TotalCount() != 2 || list.Dashboards().Len() != 2 {
|
|
t.Fatalf("AllDashboards total=%d len=%d, want 2/2", list.TotalCount(), list.Dashboards().Len())
|
|
}
|
|
|
|
// Delete the clone
|
|
del, err := c.DeleteDashboard(ctx, gen.IdReqInput{ProjectId: testProject, Id: cl.Id()})
|
|
if err != nil {
|
|
t.Fatalf("DeleteDashboard: %v", err)
|
|
}
|
|
if !del.Success() {
|
|
t.Fatalf("delete success=false")
|
|
}
|
|
if _, err := c.GetDashboard(ctx, gen.IdReqInput{ProjectId: testProject, Id: cl.Id()}); err == nil {
|
|
t.Fatalf("expected deleted dashboard to be gone")
|
|
}
|
|
t.Logf("dashboard CRUD OK: created, got, updated, cloned, listed(2), deleted")
|
|
}
|
|
|
|
// ──────────────────────────── dashboardWidgetRouter ────────────────────────
|
|
|
|
func TestWidgetCRUD(t *testing.T) {
|
|
addr, peer, stop := newService(t)
|
|
defer stop()
|
|
c, stopC := fullClient(t, addr, peer)
|
|
defer stopC()
|
|
ctx, cancel := ctx5(t)
|
|
defer cancel()
|
|
|
|
w, err := c.CreateWidget(ctx, gen.WidgetReqInput{
|
|
ProjectId: testProject, Name: "Latency", Description: "p95", View: "observations",
|
|
Dimensions: `[{"field":"model"}]`, Metrics: `[{"measure":"latency","agg":"p95"}]`,
|
|
Filters: `[]`, ChartType: "LINE_TIME_SERIES", ChartConfig: `{"type":"LINE_TIME_SERIES"}`,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreateWidget: %v", err)
|
|
}
|
|
if w.Id() == "" || w.View() != "observations" || w.Owner() != "PROJECT" {
|
|
t.Fatalf("unexpected widget: %+v", w)
|
|
}
|
|
|
|
got, err := c.GetWidget(ctx, gen.IdReqInput{ProjectId: testProject, Id: w.Id()})
|
|
if err != nil {
|
|
t.Fatalf("GetWidget: %v", err)
|
|
}
|
|
if got.Metrics() != `[{"measure":"latency","agg":"p95"}]` {
|
|
t.Fatalf("widget metrics not round-tripped: %q", got.Metrics())
|
|
}
|
|
|
|
upd, err := c.UpdateWidget(ctx, gen.WidgetReqInput{
|
|
ProjectId: testProject, WidgetId: w.Id(), Name: "Latency p99", Description: "p99",
|
|
View: "observations", Dimensions: `[]`, Metrics: `[{"measure":"latency","agg":"p99"}]`,
|
|
Filters: `[]`, ChartType: "BAR_TIME_SERIES", ChartConfig: `{"type":"BAR_TIME_SERIES"}`,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("UpdateWidget: %v", err)
|
|
}
|
|
if upd.Name() != "Latency p99" || upd.ChartType() != "BAR_TIME_SERIES" {
|
|
t.Fatalf("widget update not applied: %+v", upd)
|
|
}
|
|
|
|
cp, err := c.CopyWidgetToProject(ctx, gen.CopyWidgetReqInput{
|
|
ProjectId: testProject, WidgetId: w.Id(), DashboardId: "d1", PlacementId: "p1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CopyWidgetToProject: %v", err)
|
|
}
|
|
if !cp.Success() || cp.Id() == "" {
|
|
t.Fatalf("copy widget failed: %+v", cp)
|
|
}
|
|
|
|
list, err := c.AllWidgets(ctx, gen.ListReqInput{ProjectId: testProject, Page: 1, Limit: 50})
|
|
if err != nil {
|
|
t.Fatalf("AllWidgets: %v", err)
|
|
}
|
|
if list.TotalCount() != 2 {
|
|
t.Fatalf("AllWidgets total=%d, want 2 (original + copy)", list.TotalCount())
|
|
}
|
|
|
|
if _, err := c.DeleteWidget(ctx, gen.IdReqInput{ProjectId: testProject, Id: w.Id()}); err != nil {
|
|
t.Fatalf("DeleteWidget: %v", err)
|
|
}
|
|
t.Logf("widget CRUD OK: created, got, updated, copied, listed(2), deleted")
|
|
}
|
|
|
|
// ────────────────────────── TableViewPresetsRouter ─────────────────────────
|
|
|
|
func TestPresetCRUD(t *testing.T) {
|
|
addr, peer, stop := newService(t)
|
|
defer stop()
|
|
c, stopC := fullClient(t, addr, peer)
|
|
defer stopC()
|
|
ctx, cancel := ctx5(t)
|
|
defer cancel()
|
|
|
|
p, err := c.CreatePreset(ctx, gen.PresetReqInput{
|
|
ProjectId: testProject, Name: "My traces", TableName: "traces",
|
|
Filters: `[]`, ColumnOrder: `["id","name"]`, ColumnVisibility: `{"id":true}`,
|
|
SearchQuery: "err", OrderByColumn: "timestamp", OrderByOrder: "DESC",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreatePreset: %v", err)
|
|
}
|
|
if p.Id() == "" || p.TableName() != "traces" || p.ColumnOrder() != `["id","name"]` {
|
|
t.Fatalf("unexpected preset: %+v", p)
|
|
}
|
|
|
|
// Duplicate (projectId, tableName, name) → conflict.
|
|
if _, err := c.CreatePreset(ctx, gen.PresetReqInput{
|
|
ProjectId: testProject, Name: "My traces", TableName: "traces",
|
|
Filters: `[]`, ColumnOrder: `[]`, ColumnVisibility: `{}`,
|
|
}); err == nil {
|
|
t.Fatalf("expected duplicate preset to conflict")
|
|
}
|
|
|
|
got, err := c.GetPresetById(ctx, gen.IdReqInput{ProjectId: testProject, Id: p.Id()})
|
|
if err != nil {
|
|
t.Fatalf("GetPresetById: %v", err)
|
|
}
|
|
if got.SearchQuery() != "err" {
|
|
t.Fatalf("preset searchQuery = %q, want err", got.SearchQuery())
|
|
}
|
|
|
|
if _, err := c.UpdatePresetName(ctx, gen.PresetNameReqInput{
|
|
ProjectId: testProject, PresetId: p.Id(), Name: "My traces v2", TableName: "traces",
|
|
}); err != nil {
|
|
t.Fatalf("UpdatePresetName: %v", err)
|
|
}
|
|
|
|
byTable, err := c.GetPresetsByTableName(ctx, gen.PresetListReqInput{ProjectId: testProject, TableName: "traces"})
|
|
if err != nil {
|
|
t.Fatalf("GetPresetsByTableName: %v", err)
|
|
}
|
|
if byTable.Presets().Len() != 1 {
|
|
t.Fatalf("GetPresetsByTableName len=%d, want 1", byTable.Presets().Len())
|
|
}
|
|
|
|
link, err := c.GeneratePermalink(ctx, gen.PermalinkReqInput{
|
|
ProjectId: testProject, PresetId: p.Id(), TableName: "traces", BaseUrl: "https://app.hanzo.ai",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("GeneratePermalink: %v", err)
|
|
}
|
|
want := "https://app.hanzo.ai/project/" + testProject + "/traces?viewId=" + p.Id()
|
|
if link.Value() != want {
|
|
t.Fatalf("permalink = %q, want %q", link.Value(), want)
|
|
}
|
|
|
|
if _, err := c.DeletePreset(ctx, gen.IdReqInput{ProjectId: testProject, Id: p.Id()}); err != nil {
|
|
t.Fatalf("DeletePreset: %v", err)
|
|
}
|
|
t.Logf("preset CRUD OK: created, conflict-rejected, got, renamed, by-table(1), permalink, deleted")
|
|
}
|
|
|
|
// ─────────────────────────────── monitorsRouter ────────────────────────────
|
|
|
|
func TestMonitorCRUD(t *testing.T) {
|
|
addr, peer, stop := newService(t)
|
|
defer stop()
|
|
c, stopC := fullClient(t, addr, peer)
|
|
defer stopC()
|
|
ctx, cancel := ctx5(t)
|
|
defer cancel()
|
|
|
|
m, err := c.CreateMonitor(ctx, gen.MonitorReqInput{
|
|
ProjectId: testProject, Name: "Error rate", View: "traces",
|
|
Filters: `[]`, Metric: `{"measure":"count","agg":"count"}`, Window: `{"value":5,"unit":"minutes"}`,
|
|
ThresholdOperator: ">", AlertThreshold: 100, WarningThreshold: 50,
|
|
NoData: `{"mode":"SILENT"}`, Renotify: `{"mode":"OFF"}`, Tags: `["prod"]`, Status: "active",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreateMonitor: %v", err)
|
|
}
|
|
if m.Id() == "" || m.AlertThreshold() != 100 || m.WarningThreshold() != 50 || m.Severity() != "unknown" {
|
|
t.Fatalf("unexpected monitor: id=%q alert=%v warn=%v sev=%q", m.Id(), m.AlertThreshold(), m.WarningThreshold(), m.Severity())
|
|
}
|
|
|
|
got, err := c.GetMonitor(ctx, gen.IdReqInput{ProjectId: testProject, Id: m.Id()})
|
|
if err != nil {
|
|
t.Fatalf("GetMonitor: %v", err)
|
|
}
|
|
if got.Metric() != `{"measure":"count","agg":"count"}` || got.Status() != "active" {
|
|
t.Fatalf("monitor not round-tripped: metric=%q status=%q", got.Metric(), got.Status())
|
|
}
|
|
|
|
upd, err := c.UpdateMonitor(ctx, gen.MonitorReqInput{
|
|
ProjectId: testProject, MonitorId: m.Id(), Name: "Error rate v2", View: "traces",
|
|
Filters: `[]`, Metric: `{"measure":"count","agg":"count"}`, Window: `{"value":10,"unit":"minutes"}`,
|
|
ThresholdOperator: ">", AlertThreshold: 200, WarningThreshold: nan(),
|
|
NoData: `{"mode":"SILENT"}`, Renotify: `{"mode":"OFF"}`, Tags: `[]`, Status: "paused",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("UpdateMonitor: %v", err)
|
|
}
|
|
if upd.AlertThreshold() != 200 || upd.Status() != "paused" {
|
|
t.Fatalf("monitor update not applied: %+v", upd)
|
|
}
|
|
// A null warning threshold must come back as NaN, not 0.
|
|
if !nanEq(upd.WarningThreshold()) {
|
|
t.Fatalf("expected null warningThreshold (NaN), got %v", upd.WarningThreshold())
|
|
}
|
|
|
|
list, err := c.AllMonitors(ctx, gen.ListReqInput{ProjectId: testProject, Page: 1, Limit: 50})
|
|
if err != nil {
|
|
t.Fatalf("AllMonitors: %v", err)
|
|
}
|
|
if list.TotalCount() != 1 {
|
|
t.Fatalf("AllMonitors total=%d, want 1", list.TotalCount())
|
|
}
|
|
|
|
if _, err := c.DeleteMonitor(ctx, gen.IdReqInput{ProjectId: testProject, Id: m.Id()}); err != nil {
|
|
t.Fatalf("DeleteMonitor: %v", err)
|
|
}
|
|
t.Logf("monitor CRUD OK: created, got, updated(null-warn), listed(1), deleted")
|
|
}
|
|
|
|
// ───────────────────────────── capability gating ───────────────────────────
|
|
|
|
// TestPermissionGatingPerCategory proves each method's DashPermissions bit is
|
|
// the single chokepoint: a cap holding ONLY one category's read bit can read
|
|
// that category but is forbidden everything else (write of the same category,
|
|
// other categories).
|
|
func TestPermissionGatingPerCategory(t *testing.T) {
|
|
addr, peer, stop := newService(t)
|
|
defer stop()
|
|
ctx, cancel := ctx5(t)
|
|
defer cancel()
|
|
|
|
// Dashboard-read-only cap: AllDashboards ok, CreateDashboard forbidden,
|
|
// AllMonitors forbidden, AllWidgets ok (widgets share the dashboard bit).
|
|
cRead, stopR := newClient(t, addr, peer, server.PermDashboardRead)
|
|
defer stopR()
|
|
if _, err := cRead.AllDashboards(ctx, gen.ListReqInput{ProjectId: testProject, Page: 1, Limit: 10}); err != nil {
|
|
t.Fatalf("read-only cap should read dashboards: %v", err)
|
|
}
|
|
if _, err := cRead.AllWidgets(ctx, gen.ListReqInput{ProjectId: testProject, Page: 1, Limit: 10}); err != nil {
|
|
t.Fatalf("dashboard-read cap should read widgets (shared bit): %v", err)
|
|
}
|
|
if _, err := cRead.CreateDashboard(ctx, gen.CreateDashReqInput{ProjectId: testProject, Name: "x"}); err == nil {
|
|
t.Fatalf("read-only cap must NOT create dashboards")
|
|
}
|
|
if _, err := cRead.AllMonitors(ctx, gen.ListReqInput{ProjectId: testProject, Page: 1, Limit: 10}); err == nil {
|
|
t.Fatalf("dashboard cap must NOT read monitors")
|
|
}
|
|
|
|
// Monitor-write cap: CreateMonitor ok, CreatePreset forbidden.
|
|
cMon, stopM := newClient(t, addr, peer, server.PermMonitorWrite|server.PermMonitorRead)
|
|
defer stopM()
|
|
if _, err := cMon.CreateMonitor(ctx, gen.MonitorReqInput{
|
|
ProjectId: testProject, Name: "m", View: "traces", Filters: `[]`,
|
|
Metric: `{}`, Window: `{}`, ThresholdOperator: ">", AlertThreshold: 1, WarningThreshold: nan(),
|
|
NoData: `{}`, Renotify: `{}`, Tags: `[]`, Status: "active",
|
|
}); err != nil {
|
|
t.Fatalf("monitor-write cap should create monitors: %v", err)
|
|
}
|
|
if _, err := cMon.CreatePreset(ctx, gen.PresetReqInput{
|
|
ProjectId: testProject, Name: "p", TableName: "traces", Filters: `[]`, ColumnOrder: `[]`, ColumnVisibility: `{}`,
|
|
}); err == nil {
|
|
t.Fatalf("monitor cap must NOT create presets")
|
|
}
|
|
t.Logf("capability gating OK: per-category read/write bits enforced as the single chokepoint")
|
|
}
|
|
|
|
// TestNoCapDenied proves a cap of the WRONG kind / zero permissions is rejected
|
|
// before any data access.
|
|
func TestZeroPermissionDenied(t *testing.T) {
|
|
addr, peer, stop := newService(t)
|
|
defer stop()
|
|
c, stopC := newClient(t, addr, peer, 0) // no bits
|
|
defer stopC()
|
|
ctx, cancel := ctx5(t)
|
|
defer cancel()
|
|
if _, err := c.AllDashboards(ctx, gen.ListReqInput{ProjectId: testProject, Page: 1, Limit: 10}); err == nil {
|
|
t.Fatalf("zero-permission cap must be denied")
|
|
} else {
|
|
t.Logf("correctly denied: %v", err)
|
|
}
|
|
}
|
|
|
|
// ──────────────────────── analytics + queue TODOs (501) ────────────────────
|
|
|
|
// TestAnalyticsNotImplemented locks the contract: analytics methods are wired to
|
|
// 501 until the ClickHouse shim lands — they must NOT fabricate data.
|
|
func TestAnalyticsNotImplemented(t *testing.T) {
|
|
addr, peer, stop := newService(t)
|
|
defer stop()
|
|
c, stopC := fullClient(t, addr, peer)
|
|
defer stopC()
|
|
ctx, cancel := ctx5(t)
|
|
defer cancel()
|
|
for _, m := range []struct {
|
|
name string
|
|
method uint32
|
|
}{
|
|
{"chart", server.MethodChart},
|
|
{"scoreHistogram", server.MethodScoreHistogram},
|
|
{"executeQuery", server.MethodExecuteQuery},
|
|
{"isBatchActionInProgress", server.MethodIsBatchActionInProgress},
|
|
} {
|
|
payload := gen.NewAnalyticsReq(gen.AnalyticsReqInput{ProjectId: testProject})
|
|
if m.method == server.MethodIsBatchActionInProgress {
|
|
payload = gen.NewBatchActionReq(gen.BatchActionReqInput{ProjectId: testProject, TableName: "traces", ActionId: "a1"})
|
|
}
|
|
status := c.Probe(ctx, m.method, payload)
|
|
if status != server.StatusNotImpl {
|
|
t.Fatalf("%s: status=%d, want 501 (not-implemented TODO)", m.name, status)
|
|
}
|
|
}
|
|
t.Logf("analytics + queue methods correctly return 501 (ClickHouse/BullMQ shims pending)")
|
|
}
|
|
|
|
// ─────────────────────────────── pipelining ────────────────────────────────
|
|
|
|
// TestPipelineDashboardThenWidget is the load-bearing pipelining proof: the
|
|
// dependent CreateWidget call (pipelined off CreateDashboard's promise) must ship
|
|
// before CreateDashboard's answer resolves. We assert on the instrumented send
|
|
// log that BOTH "send" events precede the FIRST "recv" event.
|
|
func TestPipelineDashboardThenWidget(t *testing.T) {
|
|
addr, peer, stop := newService(t)
|
|
defer stop()
|
|
ctx, cancel := ctx5(t)
|
|
defer cancel()
|
|
|
|
var log []server.SendEvent
|
|
c, stopC := fullClient(t, addr, peer)
|
|
defer stopC()
|
|
c.WithSendLog(&log)
|
|
dep, stopD := fullClient(t, addr, peer)
|
|
defer stopD()
|
|
dep.WithSendLog(&log)
|
|
|
|
dash, widget, err := c.PipelineCreateDashboardThenWidget(ctx, dep,
|
|
gen.CreateDashReqInput{ProjectId: testProject, Name: "Pipelined", Description: ""},
|
|
gen.WidgetReqInput{
|
|
ProjectId: testProject, Name: "On board", View: "traces",
|
|
Dimensions: `[]`, Metrics: `[]`, Filters: `[]`, ChartType: "NUMBER", ChartConfig: `{"type":"NUMBER"}`,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Pipeline: %v", err)
|
|
}
|
|
if dash.Id() == "" || widget.Id() == "" {
|
|
t.Fatalf("pipeline produced empty ids: dash=%q widget=%q", dash.Id(), widget.Id())
|
|
}
|
|
|
|
firstRecv, sendsBefore := -1, 0
|
|
for i, e := range log {
|
|
if e.Kind == "recv" {
|
|
firstRecv = i
|
|
break
|
|
}
|
|
if e.Kind == "send" {
|
|
sendsBefore++
|
|
}
|
|
}
|
|
t.Logf("send log: %s", formatLog(log))
|
|
if firstRecv == -1 {
|
|
t.Fatalf("no recv events recorded")
|
|
}
|
|
if sendsBefore < 2 {
|
|
t.Fatalf("pipelining violated: only %d sends before first answer; want 2", sendsBefore)
|
|
}
|
|
t.Logf("PIPELINING PROVEN: %d calls (dashboard+widget) shipped before the first answer resolved", sendsBefore)
|
|
}
|
|
|
|
// ─────────────────────────────── tiny helpers ──────────────────────────────
|
|
|
|
// nan is the wire sentinel for a null float (a null Monitor warningThreshold).
|
|
func nan() float64 { return math.NaN() }
|
|
|
|
// nanEq reports whether f carries the null sentinel.
|
|
func nanEq(f float64) bool { return math.IsNaN(f) }
|
|
|
|
func itoa(n int) string {
|
|
if n == 0 {
|
|
return "0"
|
|
}
|
|
neg := n < 0
|
|
if neg {
|
|
n = -n
|
|
}
|
|
var b [12]byte
|
|
i := len(b)
|
|
for n > 0 {
|
|
i--
|
|
b[i] = byte('0' + n%10)
|
|
n /= 10
|
|
}
|
|
if neg {
|
|
i--
|
|
b[i] = '-'
|
|
}
|
|
return string(b[i:])
|
|
}
|
|
|
|
func formatLog(log []server.SendEvent) string {
|
|
out := ""
|
|
for _, e := range log {
|
|
m := "createDashboard"
|
|
if e.Method == server.MethodCreateWidget {
|
|
m = "createWidget"
|
|
}
|
|
out += e.Kind + "(" + m + ",p=" + itoa(int(e.PromiseID)) + ",t=" + itoa(int(e.Target)) + ") "
|
|
}
|
|
return out
|
|
}
|