From 9df21828e90ccb59286ef4366ea519453eb02be8 Mon Sep 17 00:00:00 2001 From: Dmitry Sergeev Date: Thu, 27 Aug 2026 23:23:10 +0300 Subject: [PATCH] add thread-safe playback status store --- imgui.ini | 4 +- internal/playback/status_store.go | 33 ++++++++ internal/playback/status_store_test.go | 108 +++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 internal/playback/status_store.go create mode 100644 internal/playback/status_store_test.go diff --git a/imgui.ini b/imgui.ini index 9730e1b..1219662 100644 --- a/imgui.ini +++ b/imgui.ini @@ -14,7 +14,7 @@ Size=200,200 Collapsed=0 [Window][Connection] -Pos=421,219 -Size=518,191 +Pos=285,503 +Size=681,205 Collapsed=0 diff --git a/internal/playback/status_store.go b/internal/playback/status_store.go new file mode 100644 index 0000000..89a20b8 --- /dev/null +++ b/internal/playback/status_store.go @@ -0,0 +1,33 @@ +package playback + +import "sync" + +type StatusStore struct { + mu sync.RWMutex + statuses map[Unit]Status +} + +func NewStatusStore() *StatusStore { + return &StatusStore{ + statuses: make(map[Unit]Status), + } +} + +func (s *StatusStore) Observe(status Status) { + s.mu.Lock() + s.statuses[status.Unit] = status + s.mu.Unlock() +} + +func (s *StatusStore) Snapshot(unit Unit) (Status, bool) { + s.mu.RLock() + status, ok := s.statuses[unit] + s.mu.RUnlock() + return status, ok +} + +func (s *StatusStore) Clear(unit Unit) { + s.mu.Lock() + delete(s.statuses, unit) + s.mu.Unlock() +} diff --git a/internal/playback/status_store_test.go b/internal/playback/status_store_test.go new file mode 100644 index 0000000..8aa308c --- /dev/null +++ b/internal/playback/status_store_test.go @@ -0,0 +1,108 @@ +package playback + +import ( + "errors" + "sync" + "testing" +) + +func TestStatusStoreSnapshotUnknownUnit(t *testing.T) { + store := NewStatusStore() + + status, ok := store.Snapshot(UnitVideo) + if ok { + t.Fatalf("Snapshot() = %#v, true; want false", status) + } +} + +func TestStatusStoreKeepsUnitsIndependent(t *testing.T) { + store := NewStatusStore() + wantVideo := Status{ + Unit: UnitVideo, + State: StateReconnecting, + Attempt: 3, + FailedAttempts: 2, + Err: errors.New("video unavailable"), + } + wantAudio := Status{ + Unit: UnitAudio, + State: StatePlaying, + Attempt: 1, + } + + store.Observe(wantVideo) + store.Observe(wantAudio) + + if got, ok := store.Snapshot(UnitVideo); !ok || got != wantVideo { + t.Fatalf("video Snapshot() = %#v, %t; want %#v, true", got, ok, wantVideo) + } + if got, ok := store.Snapshot(UnitAudio); !ok || got != wantAudio { + t.Fatalf("audio Snapshot() = %#v, %t; want %#v, true", got, ok, wantAudio) + } +} + +func TestStatusStoreObserveReplacesLatestStatus(t *testing.T) { + store := NewStatusStore() + store.Observe(Status{Unit: UnitVideo, State: StateConnecting, Attempt: 1}) + want := Status{Unit: UnitVideo, State: StatePlaying, Attempt: 2} + store.Observe(want) + + got, ok := store.Snapshot(UnitVideo) + if !ok || got != want { + t.Fatalf("Snapshot() = %#v, %t; want %#v, true", got, ok, want) + } +} + +func TestStatusStoreClearOnlySelectedUnit(t *testing.T) { + store := NewStatusStore() + wantAudio := Status{Unit: UnitAudio, State: StatePlaying} + store.Observe(Status{Unit: UnitVideo, State: StatePlaying}) + store.Observe(wantAudio) + + store.Clear(UnitVideo) + + if status, ok := store.Snapshot(UnitVideo); ok { + t.Fatalf("video Snapshot() = %#v, true after Clear", status) + } + if got, ok := store.Snapshot(UnitAudio); !ok || got != wantAudio { + t.Fatalf("audio Snapshot() = %#v, %t; want %#v, true", got, ok, wantAudio) + } +} + +func TestStatusStoreConcurrentAccess(t *testing.T) { + store := NewStatusStore() + const iterations = 1000 + + var writers sync.WaitGroup + for _, unit := range []Unit{UnitVideo, UnitAudio, UnitSync} { + unit := unit + writers.Add(1) + go func() { + defer writers.Done() + for attempt := 1; attempt <= iterations; attempt++ { + store.Observe(Status{ + Unit: unit, + State: StatePlaying, + Attempt: attempt, + }) + store.Snapshot(unit) + } + }() + } + writers.Wait() + + for _, unit := range []Unit{UnitVideo, UnitAudio, UnitSync} { + status, ok := store.Snapshot(unit) + if !ok { + t.Fatalf("Snapshot(%v) not found", unit) + } + if status.Attempt != iterations { + t.Fatalf( + "Snapshot(%v) attempt = %d, want %d", + unit, + status.Attempt, + iterations, + ) + } + } +}