thread-safe controller snapshot for GUI, CLI
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user