Files
kandev/apps/backend/internal/system/metrics/service_test.go
T
2026-06-14 22:33:34 +01:00

164 lines
3.9 KiB
Go

package metrics
import (
"context"
"sync"
"testing"
"time"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
"github.com/kandev/kandev/internal/db"
systemsettings "github.com/kandev/kandev/internal/system/settings"
ws "github.com/kandev/kandev/pkg/websocket"
)
func TestServiceSubscribeLifecycle(t *testing.T) {
svc := newTestService(t)
published := make(chan struct{}, 4)
svc.SetBroadcaster(func(_ *ws.Message) {
published <- struct{}{}
})
svc.MetricsSubscribe("client-1")
t.Cleanup(func() { unsubscribeAll(t, svc) })
waitForPublish(t, published)
svc.mu.Lock()
if len(svc.interested) != 1 {
t.Fatalf("interested=%d, want 1", len(svc.interested))
}
if svc.cancel == nil {
t.Fatal("expected sampler to start on first subscriber")
}
svc.mu.Unlock()
svc.MetricsSubscribe("client-1")
svc.mu.Lock()
if len(svc.interested) != 1 {
t.Fatalf("interested after duplicate subscribe=%d, want 1", len(svc.interested))
}
svc.mu.Unlock()
svc.MetricsSubscribe("client-2")
svc.MetricsUnsubscribe("client-1")
svc.mu.Lock()
if svc.cancel == nil {
t.Fatal("expected sampler to keep running while another subscriber remains")
}
svc.mu.Unlock()
svc.MetricsUnsubscribe("client-2")
waitForStoppedSampler(t, svc)
}
func TestServiceCollectExecutionsUsesContainerRootDiskPath(t *testing.T) {
svc := newTestService(t)
client := &recordingMetricsClient{}
svc.SetExecutionProvider(staticExecutionProvider{sources: []ExecutionSource{{
ID: "exec-1",
Label: "Executor",
Client: client,
}}})
settings := DefaultSettings()
settings.BackendDiskPath = "/mnt/backend-data"
settings.CollectExecution = true
sources := svc.collectExecutions(context.Background(), settings)
if len(sources) != 1 {
t.Fatalf("sources=%d, want 1", len(sources))
}
if client.diskPath != "/" {
t.Fatalf("disk path=%q, want /", client.diskPath)
}
}
func newTestService(t *testing.T) *Service {
t.Helper()
conn, err := sqlx.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
conn.SetMaxOpenConns(1)
t.Cleanup(func() { _ = conn.Close() })
settingsStore, err := systemsettings.NewStore(db.NewPool(conn, conn))
if err != nil {
t.Fatalf("new settings store: %v", err)
}
store := NewStore(settingsStore)
if _, err := store.SaveSettings(context.Background(), GlobalSettings{
Metrics: []string{MetricCPUPercent},
IntervalSeconds: 1,
BackendDiskPath: "/",
}); err != nil {
t.Fatalf("save settings: %v", err)
}
return NewService(store, NewCollector())
}
func waitForPublish(t *testing.T, ch <-chan struct{}) {
t.Helper()
select {
case <-ch:
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for metrics publish")
}
}
func waitForStoppedSampler(t *testing.T, svc *Service) {
t.Helper()
deadline := time.After(2 * time.Second)
ticker := time.NewTicker(10 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-deadline:
t.Fatal("timed out waiting for sampler to stop")
case <-ticker.C:
svc.mu.Lock()
interested := len(svc.interested)
cancel := svc.cancel
svc.mu.Unlock()
if interested == 0 && cancel == nil {
return
}
}
}
}
func unsubscribeAll(t *testing.T, svc *Service) {
t.Helper()
svc.mu.Lock()
clients := make([]string, 0, len(svc.interested))
for client := range svc.interested {
clients = append(clients, client)
}
svc.mu.Unlock()
for _, client := range clients {
svc.MetricsUnsubscribe(client)
}
waitForStoppedSampler(t, svc)
}
type staticExecutionProvider struct {
sources []ExecutionSource
}
func (p staticExecutionProvider) MetricExecutions() []ExecutionSource {
return p.sources
}
type recordingMetricsClient struct {
mu sync.Mutex
diskPath string
}
func (c *recordingMetricsClient) SystemMetrics(_ context.Context, _ []string, diskPath string) (*SourceSnapshot, error) {
c.mu.Lock()
defer c.mu.Unlock()
c.diskPath = diskPath
return &SourceSnapshot{Metrics: []MetricSample{}}, nil
}