thread-safe controller snapshot for GUI, CLI

This commit is contained in:
Dmitry Sergeev
2026-09-01 00:20:17 +03:00
parent ea8be34282
commit 03ebe5bbd0
2 changed files with 184 additions and 0 deletions
@@ -3,6 +3,7 @@ package playback
import (
"context"
"errors"
"sync"
"testing"
"time"
)
@@ -187,6 +188,25 @@ func receiveIndependentStarts(t *testing.T, events <-chan controllerEvent) {
}
}
func waitControllerSnapshot(
t *testing.T,
controller *SessionController,
match func(SessionSnapshot) bool,
) SessionSnapshot {
t.Helper()
deadline := time.Now().Add(time.Second)
for {
if snapshot, ok := controller.Snapshot(); ok && match(snapshot) {
return snapshot
}
if time.Now().After(deadline) {
snapshot, ok := controller.Snapshot()
t.Fatalf("snapshot timed out: %#v, available=%t", snapshot, ok)
}
time.Sleep(time.Millisecond)
}
}
func TestSessionControllerUpdatesOnlyChangedIndependentSlot(t *testing.T) {
events := make(chan controllerEvent, 32)
controller := newRecordingController(t, events)
@@ -277,3 +297,126 @@ func TestSessionControllerCancellationStopsAndJoinsRuntime(t *testing.T) {
t.Fatal("Run() did not stop after cancellation")
}
}
func TestSessionControllerSnapshotTracksDesiredPlanAndGeneration(t *testing.T) {
events := make(chan controllerEvent, 64)
controller := newRecordingController(t, events)
if snapshot, ok := controller.Snapshot(); ok {
t.Fatalf("Snapshot() before Run = %#v, true; want unavailable", snapshot)
}
initial := validCommandSession()
initial.SyncRequested = false
commands := make(chan SessionCommand)
done := make(chan error, 1)
go func() { done <- controller.Run(context.Background(), initial, commands) }()
receiveIndependentStarts(t, events)
snapshot := waitControllerSnapshot(t, controller, func(snapshot SessionSnapshot) bool {
return snapshot.Desired == initial
})
if snapshot.Generation != 1 || snapshot.Plan.Topology != TopologyIndependent {
t.Fatalf("initial snapshot = %#v", snapshot)
}
newVideo := FeedConfig{Domain: "/new-video", UUID: "new-video", Active: true}
commands <- SessionCommand{Kind: CommandSetVideo, Config: newVideo}
receiveControllerEvent(t, events)
snapshot = waitControllerSnapshot(t, controller, func(snapshot SessionSnapshot) bool {
return snapshot.Desired.Video == newVideo
})
if snapshot.Generation != 1 || snapshot.Plan.Topology != TopologyIndependent {
t.Fatalf("same-topology snapshot = %#v", snapshot)
}
commands <- SessionCommand{Kind: CommandEnableSync}
for {
if event := receiveControllerEvent(t, events); event.unit == UnitSync && event.action == "start" {
break
}
}
snapshot = waitControllerSnapshot(t, controller, func(snapshot SessionSnapshot) bool {
return snapshot.Plan.Topology == TopologySynchronized
})
if snapshot.Generation != 2 || !snapshot.Desired.SyncRequested {
t.Fatalf("sync snapshot = %#v", snapshot)
}
commands <- SessionCommand{Kind: CommandDisableSync}
for {
event := receiveControllerEvent(t, events)
if event.action == "start" && (event.unit == UnitVideo || event.unit == UnitAudio) {
break
}
}
snapshot = waitControllerSnapshot(t, controller, func(snapshot SessionSnapshot) bool {
return snapshot.Plan.Topology == TopologyIndependent && !snapshot.Desired.SyncRequested
})
if snapshot.Generation != 3 {
t.Fatalf("independent snapshot generation = %d, want 3", snapshot.Generation)
}
close(commands)
if err := <-done; err != nil {
t.Fatalf("Run() error = %v", err)
}
}
func TestSessionControllerSnapshotRetainsUnavailableSyncRequest(t *testing.T) {
events := make(chan controllerEvent, 32)
controller, err := NewSessionController(
recordingVideoSlot{events},
recordingAudioSlot{events},
recordingSyncSlot{events},
nil,
)
if err != nil {
t.Fatal(err)
}
initial := validCommandSession()
initial.SyncRequested = false
commands := make(chan SessionCommand)
done := make(chan error, 1)
go func() { done <- controller.Run(context.Background(), initial, commands) }()
receiveIndependentStarts(t, events)
commands <- SessionCommand{Kind: CommandEnableSync}
snapshot := waitControllerSnapshot(t, controller, func(snapshot SessionSnapshot) bool {
return snapshot.Desired.SyncRequested
})
if snapshot.Plan.Topology != TopologyIndependent || snapshot.Generation != 1 {
t.Fatalf("unsupported-sync snapshot = %#v", snapshot)
}
close(commands)
if err := <-done; err != nil {
t.Fatal(err)
}
}
func TestSessionControllerSnapshotConcurrentReads(t *testing.T) {
events := make(chan controllerEvent, 32)
controller := newRecordingController(t, events)
initial := validCommandSession()
initial.SyncRequested = false
commands := make(chan SessionCommand)
done := make(chan error, 1)
go func() { done <- controller.Run(context.Background(), initial, commands) }()
receiveIndependentStarts(t, events)
waitControllerSnapshot(t, controller, func(SessionSnapshot) bool { return true })
var readers sync.WaitGroup
for range 8 {
readers.Add(1)
go func() {
defer readers.Done()
for range 1_000 {
controller.Snapshot()
}
}()
}
readers.Wait()
close(commands)
if err := <-done; err != nil {
t.Fatal(err)
}
}