Refactoring #3

Merged
itten merged 87 commits from refactoring into main 2026-09-01 23:52:36 +03:00
4 changed files with 236 additions and 6 deletions
Showing only changes of commit 1e21da0aac - Show all commits
+13 -6
View File
@@ -17,8 +17,9 @@ type PlaylistController struct {
} }
type PlaylistSnapshot struct { type PlaylistSnapshot struct {
State PlaylistState State PlaylistState
Entry PlaylistEntry Entry PlaylistEntry
Revision uint64
} }
var ( var (
@@ -51,7 +52,8 @@ func (c *PlaylistController) Run(
commands <-chan PlaylistCommand, commands <-chan PlaylistCommand,
) error { ) error {
state := PlaylistState{} state := PlaylistState{}
c.publish(state) revision := uint64(0)
c.publish(state, revision)
for { for {
select { select {
@@ -78,10 +80,11 @@ func (c *PlaylistController) Run(
return ctx.Err() return ctx.Err()
case c.sessions <- sessionCommand: case c.sessions <- sessionCommand:
} }
revision++
} }
state = next state = next
c.publish(state) c.publish(state, revision)
} }
} }
} }
@@ -92,11 +95,15 @@ func (c *PlaylistController) Snapshot() (PlaylistSnapshot, bool) {
return c.snapshot, c.hasSnapshot return c.snapshot, c.hasSnapshot
} }
func (c *PlaylistController) publish(state PlaylistState) { func (c *PlaylistController) publish(state PlaylistState, revision uint64) {
entry, _ := state.Entry(c.playlist) entry, _ := state.Entry(c.playlist)
c.mu.Lock() c.mu.Lock()
c.snapshot = PlaylistSnapshot{State: state, Entry: entry} c.snapshot = PlaylistSnapshot{
State: state,
Entry: entry,
Revision: revision,
}
c.hasSnapshot = true c.hasSnapshot = true
c.mu.Unlock() c.mu.Unlock()
} }
@@ -257,6 +257,58 @@ func TestPlaylistControllerSnapshotConcurrentReads(t *testing.T) {
} }
} }
func TestPlaylistControllerSnapshotRevision(t *testing.T) {
controller, commands, sessions, cancel, result := startPlaylistController(t, navigationPlaylist(false), 8)
defer cancel()
initial := waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
return !snapshot.State.HasSelection
})
if initial.Revision != 0 {
t.Fatalf("initial revision = %d, want 0", initial.Revision)
}
commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 1}
_ = receivePlaylistSession(t, sessions)
selected := waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
return snapshot.Revision == 1
})
if selected.State.CurrentIndex != 1 {
t.Fatalf("selected state = %#v, want index 1", selected.State)
}
commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 1}
_ = receivePlaylistSession(t, sessions)
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
return snapshot.Revision == 2
})
commands <- PlaylistCommand{Kind: PlaylistSelect, Index: 2}
_ = receivePlaylistSession(t, sessions)
waitForPlaylistSnapshot(t, controller, func(snapshot PlaylistSnapshot) bool {
return snapshot.Revision == 3
})
commands <- PlaylistCommand{Kind: PlaylistNext}
time.Sleep(time.Millisecond)
boundary, ok := controller.Snapshot()
if !ok || boundary.Revision != 3 {
t.Fatalf("boundary snapshot = %#v, %v; want revision 3", boundary, ok)
}
commands <- PlaylistCommand{}
time.Sleep(time.Millisecond)
invalid, ok := controller.Snapshot()
if !ok || invalid.Revision != 3 {
t.Fatalf("invalid-command snapshot = %#v, %v; want revision 3", invalid, ok)
}
close(commands)
if err := waitForPlaylistResult(t, result); err != nil {
t.Fatalf("Run() error = %v", err)
}
}
func startPlaylistController( func startPlaylistController(
t *testing.T, t *testing.T,
playlist Playlist, playlist Playlist,
+52
View File
@@ -0,0 +1,52 @@
package playback
import "time"
type PlaylistTimingState struct {
Revision uint64
Duration time.Duration
Started bool
Deadline time.Time
}
func NewPlaylistTiming(
revision uint64,
duration time.Duration,
) PlaylistTimingState {
return PlaylistTimingState{
Revision: revision,
Duration: duration,
}
}
func StartPlaylistTiming(
current PlaylistTimingState,
revision uint64,
now time.Time,
) (PlaylistTimingState, bool) {
if revision != current.Revision || current.Duration <= 0 || current.Started {
return current, false
}
next := current
next.Started = true
next.Deadline = now.Add(current.Duration)
return next, true
}
func ExpirePlaylistTiming(
current PlaylistTimingState,
revision uint64,
now time.Time,
) (PlaylistTimingState, bool) {
if revision != current.Revision ||
!current.Started ||
now.Before(current.Deadline) {
return current, false
}
next := current
next.Started = false
next.Deadline = time.Time{}
return next, true
}
+119
View File
@@ -0,0 +1,119 @@
package playback
import (
"testing"
"time"
)
func TestNewPlaylistTimingResetsState(t *testing.T) {
got := NewPlaylistTiming(7, 10*time.Second)
want := PlaylistTimingState{Revision: 7, Duration: 10 * time.Second}
if got != want {
t.Fatalf("NewPlaylistTiming() = %#v, want %#v", got, want)
}
}
func TestStartPlaylistTiming(t *testing.T) {
now := time.Date(2026, time.September, 1, 12, 0, 0, 0, time.UTC)
current := NewPlaylistTiming(4, 10*time.Second)
got, started := StartPlaylistTiming(current, 4, now)
if !started {
t.Fatal("StartPlaylistTiming() started = false, want true")
}
want := PlaylistTimingState{
Revision: 4,
Duration: 10 * time.Second,
Started: true,
Deadline: now.Add(10 * time.Second),
}
if got != want {
t.Fatalf("StartPlaylistTiming() = %#v, want %#v", got, want)
}
}
func TestStartPlaylistTimingIgnoresInapplicableReadiness(t *testing.T) {
now := time.Date(2026, time.September, 1, 12, 0, 0, 0, time.UTC)
started, ok := StartPlaylistTiming(NewPlaylistTiming(4, time.Second), 4, now)
if !ok {
t.Fatal("initial StartPlaylistTiming() did not start")
}
tests := []struct {
name string
current PlaylistTimingState
revision uint64
}{
{name: "zero duration", current: NewPlaylistTiming(4, 0), revision: 4},
{name: "stale revision", current: NewPlaylistTiming(4, time.Second), revision: 3},
{name: "future revision", current: NewPlaylistTiming(4, time.Second), revision: 5},
{name: "already started", current: started, revision: 4},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, changed := StartPlaylistTiming(test.current, test.revision, now.Add(time.Second))
if changed {
t.Fatal("StartPlaylistTiming() changed = true, want false")
}
if got != test.current {
t.Fatalf("StartPlaylistTiming() = %#v, want unchanged %#v", got, test.current)
}
})
}
}
func TestExpirePlaylistTiming(t *testing.T) {
now := time.Date(2026, time.September, 1, 12, 0, 0, 0, time.UTC)
current, _ := StartPlaylistTiming(NewPlaylistTiming(9, 5*time.Second), 9, now)
got, expired := ExpirePlaylistTiming(current, 9, now.Add(5*time.Second))
if !expired {
t.Fatal("ExpirePlaylistTiming() expired = false, want true")
}
want := PlaylistTimingState{Revision: 9, Duration: 5 * time.Second}
if got != want {
t.Fatalf("ExpirePlaylistTiming() = %#v, want %#v", got, want)
}
again, expired := ExpirePlaylistTiming(got, 9, now.Add(6*time.Second))
if expired || again != got {
t.Fatalf("duplicate ExpirePlaylistTiming() = %#v, %v; want unchanged, false", again, expired)
}
}
func TestExpirePlaylistTimingIgnoresInapplicableEvents(t *testing.T) {
now := time.Date(2026, time.September, 1, 12, 0, 0, 0, time.UTC)
current, _ := StartPlaylistTiming(NewPlaylistTiming(4, 10*time.Second), 4, now)
tests := []struct {
name string
revision uint64
at time.Time
}{
{name: "early", revision: 4, at: now.Add(9 * time.Second)},
{name: "stale revision", revision: 3, at: now.Add(10 * time.Second)},
{name: "future revision", revision: 5, at: now.Add(10 * time.Second)},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, expired := ExpirePlaylistTiming(current, test.revision, test.at)
if expired {
t.Fatal("ExpirePlaylistTiming() expired = true, want false")
}
if got != current {
t.Fatalf("ExpirePlaylistTiming() = %#v, want unchanged %#v", got, current)
}
})
}
}
func TestNewPlaylistTimingInvalidatesPreviousDeadline(t *testing.T) {
now := time.Date(2026, time.September, 1, 12, 0, 0, 0, time.UTC)
old, _ := StartPlaylistTiming(NewPlaylistTiming(1, time.Second), 1, now)
current := NewPlaylistTiming(2, 2*time.Second)
got, expired := ExpirePlaylistTiming(current, old.Revision, old.Deadline)
if expired || got != current {
t.Fatalf("old expiry changed new timing: %#v, %v", got, expired)
}
}