feat: add player state machine core
- commit
- 6cf858605d13149f6973416c140589bbc468c86d
- author
- brice · · 2026-04-09 14:55 UTC
- parent
- acd608d
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
10 files · +258 -0
M cmd/pipewiregen/internal/parser/parser.go +17 -0
| @@ -71,5 +71,22 @@ func validate(m *model.Model) error { | |||
| 71 | 71 | } | |
| 72 | 72 | } | |
| 73 | 73 | ||
| 74 | + | // Validate callbacks | |
| 75 | + | for i, cb := range m.Callbacks { | |
| 76 | + | if cb.Name == "" { | |
| 77 | + | return fmt.Errorf("callback at index %d has empty name", i) | |
| 78 | + | } | |
| 79 | + | if cb.Signature == "" { | |
| 80 | + | return fmt.Errorf("callback %q has empty signature", cb.Name) | |
| 81 | + | } | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | // Validate event structs | |
| 85 | + | for i, es := range m.EventStructs { | |
| 86 | + | if es.Name == "" { | |
| 87 | + | return fmt.Errorf("event struct at index %d has empty name", i) | |
| 88 | + | } | |
| 89 | + | } | |
| 90 | + | ||
| 74 | 91 | return nil | |
| 75 | 92 | } | |
| @@ -71,5 +71,22 @@ func validate(m *model.Model) error { | |||||
| 71 | } | 71 | } | ||
| 72 | } | 72 | } | ||
| 73 | 73 | ||||
| 74 | + | // Validate callbacks | |||
| 75 | + | for i, cb := range m.Callbacks { | |||
| 76 | + | if cb.Name == "" { | |||
| 77 | + | return fmt.Errorf("callback at index %d has empty name", i) | |||
| 78 | + | } | |||
| 79 | + | if cb.Signature == "" { | |||
| 80 | + | return fmt.Errorf("callback %q has empty signature", cb.Name) | |||
| 81 | + | } | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | // Validate event structs | |||
| 85 | + | for i, es := range m.EventStructs { | |||
| 86 | + | if es.Name == "" { | |||
| 87 | + | return fmt.Errorf("event struct at index %d has empty name", i) | |||
| 88 | + | } | |||
| 89 | + | } | |||
| 90 | + | ||||
| 74 | return nil | 91 | return nil | ||
| 75 | } | 92 | } | ||
M cmd/pipewiregen/internal/parser/parser_test.go +39 -0
| @@ -112,3 +112,42 @@ func TestLoadEmptySymbolSignatureReturnsError(t *testing.T) { | |||
| 112 | 112 | t.Fatalf("error message should contain 'empty signature', got: %v", err) | |
| 113 | 113 | } | |
| 114 | 114 | } | |
| 115 | + | ||
| 116 | + | func TestLoadEmptyCallbackNameReturnsError(t *testing.T) { | |
| 117 | + | _, err := Load("testdata/empty_callback_name.json") | |
| 118 | + | if err == nil { | |
| 119 | + | t.Fatal("expected error for empty callback name, got nil") | |
| 120 | + | } | |
| 121 | + | if !strings.Contains(err.Error(), "validation failed") { | |
| 122 | + | t.Fatalf("error message should contain 'validation failed', got: %v", err) | |
| 123 | + | } | |
| 124 | + | if !strings.Contains(err.Error(), "callback") && !strings.Contains(err.Error(), "empty name") { | |
| 125 | + | t.Fatalf("error message should mention callback empty name, got: %v", err) | |
| 126 | + | } | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | func TestLoadEmptyCallbackSignatureReturnsError(t *testing.T) { | |
| 130 | + | _, err := Load("testdata/empty_callback_signature.json") | |
| 131 | + | if err == nil { | |
| 132 | + | t.Fatal("expected error for empty callback signature, got nil") | |
| 133 | + | } | |
| 134 | + | if !strings.Contains(err.Error(), "validation failed") { | |
| 135 | + | t.Fatalf("error message should contain 'validation failed', got: %v", err) | |
| 136 | + | } | |
| 137 | + | if !strings.Contains(err.Error(), "callback") && !strings.Contains(err.Error(), "empty signature") { | |
| 138 | + | t.Fatalf("error message should mention callback empty signature, got: %v", err) | |
| 139 | + | } | |
| 140 | + | } | |
| 141 | + | ||
| 142 | + | func TestLoadEmptyEventStructNameReturnsError(t *testing.T) { | |
| 143 | + | _, err := Load("testdata/empty_event_struct_name.json") | |
| 144 | + | if err == nil { | |
| 145 | + | t.Fatal("expected error for empty event struct name, got nil") | |
| 146 | + | } | |
| 147 | + | if !strings.Contains(err.Error(), "validation failed") { | |
| 148 | + | t.Fatalf("error message should contain 'validation failed', got: %v", err) | |
| 149 | + | } | |
| 150 | + | if !strings.Contains(err.Error(), "event struct") && !strings.Contains(err.Error(), "empty name") { | |
| 151 | + | t.Fatalf("error message should mention event struct empty name, got: %v", err) | |
| 152 | + | } | |
| 153 | + | } | |
| @@ -112,3 +112,42 @@ func TestLoadEmptySymbolSignatureReturnsError(t *testing.T) { | |||||
| 112 | t.Fatalf("error message should contain 'empty signature', got: %v", err) | 112 | t.Fatalf("error message should contain 'empty signature', got: %v", err) | ||
| 113 | } | 113 | } | ||
| 114 | } | 114 | } | ||
| 115 | + | ||||
| 116 | + | func TestLoadEmptyCallbackNameReturnsError(t *testing.T) { | |||
| 117 | + | _, err := Load("testdata/empty_callback_name.json") | |||
| 118 | + | if err == nil { | |||
| 119 | + | t.Fatal("expected error for empty callback name, got nil") | |||
| 120 | + | } | |||
| 121 | + | if !strings.Contains(err.Error(), "validation failed") { | |||
| 122 | + | t.Fatalf("error message should contain 'validation failed', got: %v", err) | |||
| 123 | + | } | |||
| 124 | + | if !strings.Contains(err.Error(), "callback") && !strings.Contains(err.Error(), "empty name") { | |||
| 125 | + | t.Fatalf("error message should mention callback empty name, got: %v", err) | |||
| 126 | + | } | |||
| 127 | + | } | |||
| 128 | + | ||||
| 129 | + | func TestLoadEmptyCallbackSignatureReturnsError(t *testing.T) { | |||
| 130 | + | _, err := Load("testdata/empty_callback_signature.json") | |||
| 131 | + | if err == nil { | |||
| 132 | + | t.Fatal("expected error for empty callback signature, got nil") | |||
| 133 | + | } | |||
| 134 | + | if !strings.Contains(err.Error(), "validation failed") { | |||
| 135 | + | t.Fatalf("error message should contain 'validation failed', got: %v", err) | |||
| 136 | + | } | |||
| 137 | + | if !strings.Contains(err.Error(), "callback") && !strings.Contains(err.Error(), "empty signature") { | |||
| 138 | + | t.Fatalf("error message should mention callback empty signature, got: %v", err) | |||
| 139 | + | } | |||
| 140 | + | } | |||
| 141 | + | ||||
| 142 | + | func TestLoadEmptyEventStructNameReturnsError(t *testing.T) { | |||
| 143 | + | _, err := Load("testdata/empty_event_struct_name.json") | |||
| 144 | + | if err == nil { | |||
| 145 | + | t.Fatal("expected error for empty event struct name, got nil") | |||
| 146 | + | } | |||
| 147 | + | if !strings.Contains(err.Error(), "validation failed") { | |||
| 148 | + | t.Fatalf("error message should contain 'validation failed', got: %v", err) | |||
| 149 | + | } | |||
| 150 | + | if !strings.Contains(err.Error(), "event struct") && !strings.Contains(err.Error(), "empty name") { | |||
| 151 | + | t.Fatalf("error message should mention event struct empty name, got: %v", err) | |||
| 152 | + | } | |||
| 153 | + | } | |||
A cmd/pipewiregen/internal/parser/testdata/empty_callback_name.json +14 -0
| @@ -0,0 +1,14 @@ | |||
| 1 | + | { | |
| 2 | + | "libraries": [ | |
| 3 | + | {"name": "pipewire", "soname": "libpipewire-0.3.so.0"} | |
| 4 | + | ], | |
| 5 | + | "groups": [ | |
| 6 | + | {"name": "init", "interface": "InitAPI", "package": "out", "symbols": ["pw_init"]} | |
| 7 | + | ], | |
| 8 | + | "symbols": [ | |
| 9 | + | {"name": "pw_init", "library": "pipewire", "group": "init", "signature": "func()"} | |
| 10 | + | ], | |
| 11 | + | "callbacks": [ | |
| 12 | + | {"name": "", "signature": "struct{}"} | |
| 13 | + | ] | |
| 14 | + | } | |
| @@ -0,0 +1,14 @@ | |||||
| 1 | + | { | |||
| 2 | + | "libraries": [ | |||
| 3 | + | {"name": "pipewire", "soname": "libpipewire-0.3.so.0"} | |||
| 4 | + | ], | |||
| 5 | + | "groups": [ | |||
| 6 | + | {"name": "init", "interface": "InitAPI", "package": "out", "symbols": ["pw_init"]} | |||
| 7 | + | ], | |||
| 8 | + | "symbols": [ | |||
| 9 | + | {"name": "pw_init", "library": "pipewire", "group": "init", "signature": "func()"} | |||
| 10 | + | ], | |||
| 11 | + | "callbacks": [ | |||
| 12 | + | {"name": "", "signature": "struct{}"} | |||
| 13 | + | ] | |||
| 14 | + | } | |||
A cmd/pipewiregen/internal/parser/testdata/empty_callback_signature.json +14 -0
| @@ -0,0 +1,14 @@ | |||
| 1 | + | { | |
| 2 | + | "libraries": [ | |
| 3 | + | {"name": "pipewire", "soname": "libpipewire-0.3.so.0"} | |
| 4 | + | ], | |
| 5 | + | "groups": [ | |
| 6 | + | {"name": "init", "interface": "InitAPI", "package": "out", "symbols": ["pw_init"]} | |
| 7 | + | ], | |
| 8 | + | "symbols": [ | |
| 9 | + | {"name": "pw_init", "library": "pipewire", "group": "init", "signature": "func()"} | |
| 10 | + | ], | |
| 11 | + | "callbacks": [ | |
| 12 | + | {"name": "pw_stream_events", "signature": ""} | |
| 13 | + | ] | |
| 14 | + | } | |
| @@ -0,0 +1,14 @@ | |||||
| 1 | + | { | |||
| 2 | + | "libraries": [ | |||
| 3 | + | {"name": "pipewire", "soname": "libpipewire-0.3.so.0"} | |||
| 4 | + | ], | |||
| 5 | + | "groups": [ | |||
| 6 | + | {"name": "init", "interface": "InitAPI", "package": "out", "symbols": ["pw_init"]} | |||
| 7 | + | ], | |||
| 8 | + | "symbols": [ | |||
| 9 | + | {"name": "pw_init", "library": "pipewire", "group": "init", "signature": "func()"} | |||
| 10 | + | ], | |||
| 11 | + | "callbacks": [ | |||
| 12 | + | {"name": "pw_stream_events", "signature": ""} | |||
| 13 | + | ] | |||
| 14 | + | } | |||
A cmd/pipewiregen/internal/parser/testdata/empty_event_struct_name.json +14 -0
| @@ -0,0 +1,14 @@ | |||
| 1 | + | { | |
| 2 | + | "libraries": [ | |
| 3 | + | {"name": "pipewire", "soname": "libpipewire-0.3.so.0"} | |
| 4 | + | ], | |
| 5 | + | "groups": [ | |
| 6 | + | {"name": "init", "interface": "InitAPI", "package": "out", "symbols": ["pw_init"]} | |
| 7 | + | ], | |
| 8 | + | "symbols": [ | |
| 9 | + | {"name": "pw_init", "library": "pipewire", "group": "init", "signature": "func()"} | |
| 10 | + | ], | |
| 11 | + | "event_structs": [ | |
| 12 | + | {"name": "", "callbacks": ["process"]} | |
| 13 | + | ] | |
| 14 | + | } | |
| @@ -0,0 +1,14 @@ | |||||
| 1 | + | { | |||
| 2 | + | "libraries": [ | |||
| 3 | + | {"name": "pipewire", "soname": "libpipewire-0.3.so.0"} | |||
| 4 | + | ], | |||
| 5 | + | "groups": [ | |||
| 6 | + | {"name": "init", "interface": "InitAPI", "package": "out", "symbols": ["pw_init"]} | |||
| 7 | + | ], | |||
| 8 | + | "symbols": [ | |||
| 9 | + | {"name": "pw_init", "library": "pipewire", "group": "init", "signature": "func()"} | |||
| 10 | + | ], | |||
| 11 | + | "event_structs": [ | |||
| 12 | + | {"name": "", "callbacks": ["process"]} | |||
| 13 | + | ] | |||
| 14 | + | } | |||
M internal/capi/init_gen.go +1 -0 generated file: collapsed
M internal/capi/loop_gen.go +1 -0 generated file: collapsed
A internal/core/player.go +95 -0
| @@ -0,0 +1,95 @@ | |||
| 1 | + | package core | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "errors" | |
| 5 | + | "sync" | |
| 6 | + | "sync/atomic" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | var ErrInvalidPlayerState = errors.New("invalid player state transition") | |
| 10 | + | ||
| 11 | + | // playerConfig holds configuration for the player | |
| 12 | + | type playerConfig struct { | |
| 13 | + | // Minimal for now, will be extended in future tasks | |
| 14 | + | } | |
| 15 | + | ||
| 16 | + | // playerCallbacks holds callbacks for player events | |
| 17 | + | type playerCallbacks struct { | |
| 18 | + | OnStateChange func(oldState, newState PlayerState) | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | // player is the internal player shell with state machine | |
| 22 | + | type player struct { | |
| 23 | + | config playerConfig | |
| 24 | + | callbacks playerCallbacks | |
| 25 | + | state atomic.Int32 | |
| 26 | + | terminal atomic.Bool | |
| 27 | + | mu sync.Mutex | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | // newPlayer creates a new player instance | |
| 31 | + | func newPlayer(config playerConfig, callbacks playerCallbacks) *player { | |
| 32 | + | p := &player{ | |
| 33 | + | config: config, | |
| 34 | + | callbacks: callbacks, | |
| 35 | + | } | |
| 36 | + | p.state.Store(int32(PlayerStateIdle)) | |
| 37 | + | return p | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | // State returns the current player state | |
| 41 | + | func (p *player) State() PlayerState { | |
| 42 | + | return PlayerState(p.state.Load()) | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | // setState atomically sets the player state | |
| 46 | + | func (p *player) setState(next PlayerState) { | |
| 47 | + | p.state.Store(int32(next)) | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | // transition attempts to transition to the next state | |
| 51 | + | func (p *player) transition(next PlayerState) error { | |
| 52 | + | p.mu.Lock() | |
| 53 | + | defer p.mu.Unlock() | |
| 54 | + | ||
| 55 | + | current := p.State() | |
| 56 | + | ||
| 57 | + | // Validate transition | |
| 58 | + | if !isValidTransition(current, next) { | |
| 59 | + | return ErrInvalidPlayerState | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | // Perform transition | |
| 63 | + | p.setState(next) | |
| 64 | + | ||
| 65 | + | // Notify callback | |
| 66 | + | if p.callbacks.OnStateChange != nil { | |
| 67 | + | p.callbacks.OnStateChange(current, next) | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | return nil | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | // isValidTransition checks if a state transition is valid | |
| 74 | + | func isValidTransition(current, next PlayerState) bool { | |
| 75 | + | switch current { | |
| 76 | + | case PlayerStateIdle: | |
| 77 | + | return next == PlayerStateStarting | |
| 78 | + | case PlayerStateStarting: | |
| 79 | + | return next == PlayerStatePlaying || next == PlayerStateError | |
| 80 | + | case PlayerStatePlaying: | |
| 81 | + | return next == PlayerStatePaused || next == PlayerStateStopped || next == PlayerStateClosing | |
| 82 | + | case PlayerStatePaused: | |
| 83 | + | return next == PlayerStatePlaying || next == PlayerStateStopped || next == PlayerStateClosing | |
| 84 | + | case PlayerStateStopped: | |
| 85 | + | return next == PlayerStateStarting || next == PlayerStateClosing | |
| 86 | + | case PlayerStateClosing: | |
| 87 | + | return next == PlayerStateClosed | |
| 88 | + | case PlayerStateClosed: | |
| 89 | + | return false // Terminal state | |
| 90 | + | case PlayerStateError: | |
| 91 | + | return next == PlayerStateClosing | |
| 92 | + | default: | |
| 93 | + | return false | |
| 94 | + | } | |
| 95 | + | } | |
| @@ -0,0 +1,95 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "errors" | |||
| 5 | + | "sync" | |||
| 6 | + | "sync/atomic" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | var ErrInvalidPlayerState = errors.New("invalid player state transition") | |||
| 10 | + | ||||
| 11 | + | // playerConfig holds configuration for the player | |||
| 12 | + | type playerConfig struct { | |||
| 13 | + | // Minimal for now, will be extended in future tasks | |||
| 14 | + | } | |||
| 15 | + | ||||
| 16 | + | // playerCallbacks holds callbacks for player events | |||
| 17 | + | type playerCallbacks struct { | |||
| 18 | + | OnStateChange func(oldState, newState PlayerState) | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | // player is the internal player shell with state machine | |||
| 22 | + | type player struct { | |||
| 23 | + | config playerConfig | |||
| 24 | + | callbacks playerCallbacks | |||
| 25 | + | state atomic.Int32 | |||
| 26 | + | terminal atomic.Bool | |||
| 27 | + | mu sync.Mutex | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | // newPlayer creates a new player instance | |||
| 31 | + | func newPlayer(config playerConfig, callbacks playerCallbacks) *player { | |||
| 32 | + | p := &player{ | |||
| 33 | + | config: config, | |||
| 34 | + | callbacks: callbacks, | |||
| 35 | + | } | |||
| 36 | + | p.state.Store(int32(PlayerStateIdle)) | |||
| 37 | + | return p | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | // State returns the current player state | |||
| 41 | + | func (p *player) State() PlayerState { | |||
| 42 | + | return PlayerState(p.state.Load()) | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | // setState atomically sets the player state | |||
| 46 | + | func (p *player) setState(next PlayerState) { | |||
| 47 | + | p.state.Store(int32(next)) | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | // transition attempts to transition to the next state | |||
| 51 | + | func (p *player) transition(next PlayerState) error { | |||
| 52 | + | p.mu.Lock() | |||
| 53 | + | defer p.mu.Unlock() | |||
| 54 | + | ||||
| 55 | + | current := p.State() | |||
| 56 | + | ||||
| 57 | + | // Validate transition | |||
| 58 | + | if !isValidTransition(current, next) { | |||
| 59 | + | return ErrInvalidPlayerState | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | // Perform transition | |||
| 63 | + | p.setState(next) | |||
| 64 | + | ||||
| 65 | + | // Notify callback | |||
| 66 | + | if p.callbacks.OnStateChange != nil { | |||
| 67 | + | p.callbacks.OnStateChange(current, next) | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | return nil | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | // isValidTransition checks if a state transition is valid | |||
| 74 | + | func isValidTransition(current, next PlayerState) bool { | |||
| 75 | + | switch current { | |||
| 76 | + | case PlayerStateIdle: | |||
| 77 | + | return next == PlayerStateStarting | |||
| 78 | + | case PlayerStateStarting: | |||
| 79 | + | return next == PlayerStatePlaying || next == PlayerStateError | |||
| 80 | + | case PlayerStatePlaying: | |||
| 81 | + | return next == PlayerStatePaused || next == PlayerStateStopped || next == PlayerStateClosing | |||
| 82 | + | case PlayerStatePaused: | |||
| 83 | + | return next == PlayerStatePlaying || next == PlayerStateStopped || next == PlayerStateClosing | |||
| 84 | + | case PlayerStateStopped: | |||
| 85 | + | return next == PlayerStateStarting || next == PlayerStateClosing | |||
| 86 | + | case PlayerStateClosing: | |||
| 87 | + | return next == PlayerStateClosed | |||
| 88 | + | case PlayerStateClosed: | |||
| 89 | + | return false // Terminal state | |||
| 90 | + | case PlayerStateError: | |||
| 91 | + | return next == PlayerStateClosing | |||
| 92 | + | default: | |||
| 93 | + | return false | |||
| 94 | + | } | |||
| 95 | + | } | |||
A internal/core/player_state.go +15 -0
| @@ -0,0 +1,15 @@ | |||
| 1 | + | package core | |
| 2 | + | ||
| 3 | + | // PlayerState represents the current state of the player | |
| 4 | + | type PlayerState int32 | |
| 5 | + | ||
| 6 | + | const ( | |
| 7 | + | PlayerStateIdle PlayerState = iota | |
| 8 | + | PlayerStateStarting | |
| 9 | + | PlayerStatePlaying | |
| 10 | + | PlayerStatePaused | |
| 11 | + | PlayerStateStopped | |
| 12 | + | PlayerStateClosing | |
| 13 | + | PlayerStateClosed | |
| 14 | + | PlayerStateError | |
| 15 | + | ) | |
| @@ -0,0 +1,15 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | // PlayerState represents the current state of the player | |||
| 4 | + | type PlayerState int32 | |||
| 5 | + | ||||
| 6 | + | const ( | |||
| 7 | + | PlayerStateIdle PlayerState = iota | |||
| 8 | + | PlayerStateStarting | |||
| 9 | + | PlayerStatePlaying | |||
| 10 | + | PlayerStatePaused | |||
| 11 | + | PlayerStateStopped | |||
| 12 | + | PlayerStateClosing | |||
| 13 | + | PlayerStateClosed | |||
| 14 | + | PlayerStateError | |||
| 15 | + | ) | |||
A internal/core/player_state_test.go +48 -0
| @@ -0,0 +1,48 @@ | |||
| 1 | + | package core | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ) | |
| 6 | + | ||
| 7 | + | func TestPlayerStateTransitionsIdleStartStopClose(t *testing.T) { | |
| 8 | + | p := newPlayer(playerConfig{}, playerCallbacks{}) | |
| 9 | + | ||
| 10 | + | if got := p.State(); got != PlayerStateIdle { | |
| 11 | + | t.Errorf("initial state = %v, want %v", got, PlayerStateIdle) | |
| 12 | + | } | |
| 13 | + | ||
| 14 | + | if err := p.transition(PlayerStateStarting); err != nil { | |
| 15 | + | t.Errorf("transition to Starting failed: %v", err) | |
| 16 | + | } | |
| 17 | + | if got := p.State(); got != PlayerStateStarting { | |
| 18 | + | t.Errorf("after transition to Starting, state = %v, want %v", got, PlayerStateStarting) | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | if err := p.transition(PlayerStatePlaying); err != nil { | |
| 22 | + | t.Errorf("transition to Playing failed: %v", err) | |
| 23 | + | } | |
| 24 | + | if got := p.State(); got != PlayerStatePlaying { | |
| 25 | + | t.Errorf("after transition to Playing, state = %v, want %v", got, PlayerStatePlaying) | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | if err := p.transition(PlayerStateStopped); err != nil { | |
| 29 | + | t.Errorf("transition to Stopped failed: %v", err) | |
| 30 | + | } | |
| 31 | + | if got := p.State(); got != PlayerStateStopped { | |
| 32 | + | t.Errorf("after transition to Stopped, state = %v, want %v", got, PlayerStateStopped) | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | if err := p.transition(PlayerStateClosing); err != nil { | |
| 36 | + | t.Errorf("transition to Closing failed: %v", err) | |
| 37 | + | } | |
| 38 | + | if got := p.State(); got != PlayerStateClosing { | |
| 39 | + | t.Errorf("after transition to Closing, state = %v, want %v", got, PlayerStateClosing) | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | if err := p.transition(PlayerStateClosed); err != nil { | |
| 43 | + | t.Errorf("transition to Closed failed: %v", err) | |
| 44 | + | } | |
| 45 | + | if got := p.State(); got != PlayerStateClosed { | |
| 46 | + | t.Errorf("after transition to Closed, state = %v, want %v", got, PlayerStateClosed) | |
| 47 | + | } | |
| 48 | + | } | |
| @@ -0,0 +1,48 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ) | |||
| 6 | + | ||||
| 7 | + | func TestPlayerStateTransitionsIdleStartStopClose(t *testing.T) { | |||
| 8 | + | p := newPlayer(playerConfig{}, playerCallbacks{}) | |||
| 9 | + | ||||
| 10 | + | if got := p.State(); got != PlayerStateIdle { | |||
| 11 | + | t.Errorf("initial state = %v, want %v", got, PlayerStateIdle) | |||
| 12 | + | } | |||
| 13 | + | ||||
| 14 | + | if err := p.transition(PlayerStateStarting); err != nil { | |||
| 15 | + | t.Errorf("transition to Starting failed: %v", err) | |||
| 16 | + | } | |||
| 17 | + | if got := p.State(); got != PlayerStateStarting { | |||
| 18 | + | t.Errorf("after transition to Starting, state = %v, want %v", got, PlayerStateStarting) | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | if err := p.transition(PlayerStatePlaying); err != nil { | |||
| 22 | + | t.Errorf("transition to Playing failed: %v", err) | |||
| 23 | + | } | |||
| 24 | + | if got := p.State(); got != PlayerStatePlaying { | |||
| 25 | + | t.Errorf("after transition to Playing, state = %v, want %v", got, PlayerStatePlaying) | |||
| 26 | + | } | |||
| 27 | + | ||||
| 28 | + | if err := p.transition(PlayerStateStopped); err != nil { | |||
| 29 | + | t.Errorf("transition to Stopped failed: %v", err) | |||
| 30 | + | } | |||
| 31 | + | if got := p.State(); got != PlayerStateStopped { | |||
| 32 | + | t.Errorf("after transition to Stopped, state = %v, want %v", got, PlayerStateStopped) | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | if err := p.transition(PlayerStateClosing); err != nil { | |||
| 36 | + | t.Errorf("transition to Closing failed: %v", err) | |||
| 37 | + | } | |||
| 38 | + | if got := p.State(); got != PlayerStateClosing { | |||
| 39 | + | t.Errorf("after transition to Closing, state = %v, want %v", got, PlayerStateClosing) | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | if err := p.transition(PlayerStateClosed); err != nil { | |||
| 43 | + | t.Errorf("transition to Closed failed: %v", err) | |||
| 44 | + | } | |||
| 45 | + | if got := p.State(); got != PlayerStateClosed { | |||
| 46 | + | t.Errorf("after transition to Closed, state = %v, want %v", got, PlayerStateClosed) | |||
| 47 | + | } | |||
| 48 | + | } | |||