131 lines
4.6 KiB
Go
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)
|
|
}
|
|
}
|