Files

131 lines
4.6 KiB
Go

package media_shard
import (
"context"
"errors"
"testing"
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type testRoute struct{ ID, DeviceID, ProfileToken, Path, Desired, Actual string }
func (testRoute) TableName() string { return "sense_media_routes" }
type testDevice struct{ ID, Name, Location string }
func (testDevice) TableName() string { return "sense_devices" }
func testService(t *testing.T) (*Service, *gorm.DB) {
t.Helper()
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err = db.AutoMigrate(&Shard{}, &Assignment{}, &testRoute{}, &testDevice{}); err != nil {
t.Fatal(err)
}
now := time.Date(2026, 8, 28, 8, 0, 0, 0, time.UTC)
service := NewService(db, func(context.Context, string) error { return nil })
service.Now = func() time.Time { return now }
return service, db
}
func TestStableAssignmentUsesConfiguredCapacity(t *testing.T) {
service, _ := testService(t)
ctx := context.Background()
if err := service.SyncSpecs(ctx, []Spec{{ID: "alpha", Name: "A", Mode: "external", ControlAPI: "http://127.0.0.1:9997", Capacity: 3}, {ID: "beta", Name: "B", Mode: "external", ControlAPI: "http://127.0.0.1:19997", Capacity: 2}}); err != nil {
t.Fatal(err)
}
if err := service.RefreshAll(ctx); err != nil {
t.Fatal(err)
}
first, err := service.EnsureAssignment(ctx, "route-1")
if err != nil {
t.Fatal(err)
}
second, err := service.EnsureAssignment(ctx, "route-1")
if err != nil {
t.Fatal(err)
}
if first.ID != second.ID {
t.Fatalf("assignment changed from %s to %s", first.ID, second.ID)
}
for _, id := range []string{"route-2", "route-3", "route-4", "route-5"} {
if _, err = service.EnsureAssignment(ctx, id); err != nil {
t.Fatalf("assign %s: %v", id, err)
}
}
if _, err = service.EnsureAssignment(ctx, "route-6"); !errors.Is(err, ErrNoCapacity) {
t.Fatalf("expected configured capacity error, got %v", err)
}
}
func TestFailureKeepsOwnershipAndLocatesImpact(t *testing.T) {
service, db := testService(t)
ctx := context.Background()
if err := service.SyncSpecs(ctx, []Spec{{ID: "failed", Name: "故障分片", Mode: "external", ControlAPI: "http://127.0.0.1:9997", Capacity: 7}}); err != nil {
t.Fatal(err)
}
if err := service.RefreshAll(ctx); err != nil {
t.Fatal(err)
}
if err := db.Create(&testDevice{ID: "device-1", Name: "东门摄像机", Location: "东门"}).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&testRoute{ID: "device-1:main", DeviceID: "device-1", ProfileToken: "main", Path: "sense_abc", Desired: "running", Actual: "ready"}).Error; err != nil {
t.Fatal(err)
}
if _, err := service.EnsureAssignment(ctx, "device-1:main"); err != nil {
t.Fatal(err)
}
service.Probe = func(context.Context, string) error { return errors.New("offline") }
if err := service.RefreshAll(ctx); err != nil {
t.Fatal(err)
}
detail, err := service.Get(ctx, "failed")
if err != nil {
t.Fatal(err)
}
if detail.Status != StatusFailed || len(detail.Impact) != 1 || detail.Impact[0].DeviceName != "东门摄像机" || detail.Impact[0].ProfileToken != "main" {
t.Fatalf("unexpected detail: %+v", detail)
}
resolved, err := service.ResolveRoute(ctx, "device-1:main")
if err != nil || resolved.ID != "failed" {
t.Fatalf("failure rewrote ownership: %+v %v", resolved, err)
}
}
func TestMigrationPreflightIsReadOnly(t *testing.T) {
service, db := testService(t)
ctx := context.Background()
if err := service.SyncSpecs(ctx, []Spec{{ID: "source", Name: "源", Mode: "external", ControlAPI: "http://127.0.0.1:9997", Capacity: 1}, {ID: "target", Name: "目标", Mode: "external", ControlAPI: "http://127.0.0.1:19997", Capacity: 4}}); err != nil {
t.Fatal(err)
}
if err := service.RefreshAll(ctx); err != nil {
t.Fatal(err)
}
if err := db.Create(&testRoute{ID: "route", DeviceID: "d", ProfileToken: "p", Path: "path", Desired: "running", Actual: "ready"}).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&Assignment{RouteID: "route", ShardID: "source", Algorithm: "rendezvous-v1", CreatedAt: service.now(), UpdatedAt: service.now()}).Error; err != nil {
t.Fatal(err)
}
before := Assignment{}
db.First(&before, "route_id = ?", "route")
result, err := service.Preflight(ctx, "source")
if err != nil {
t.Fatal(err)
}
after := Assignment{}
db.First(&after, "route_id = ?", "route")
if !result.Ready || result.ExecutionAuthorized || !result.RequiresIssue || result.TargetShardID != "target" {
t.Fatalf("unexpected preflight: %+v", result)
}
if before.ShardID != after.ShardID || !before.UpdatedAt.Equal(after.UpdatedAt) {
t.Fatalf("preflight mutated assignment: before=%+v after=%+v", before, after)
}
}