bugfixes and refactoring in game-server
This commit is contained in:
@@ -237,7 +237,9 @@ func TestAckThenReconnectResendsCurrentState(t *testing.T) {
|
||||
|
||||
// Player 1 acknowledges the STATE message, then reconnects. The queue
|
||||
// is empty, so the server re-sends the current state with a fresh seq.
|
||||
m.Ack(match, 0, 1)
|
||||
if err := m.Ack(match, 0, 1); err != nil {
|
||||
t.Fatalf("ack: %v", err)
|
||||
}
|
||||
|
||||
fresh := &fakeConn{}
|
||||
if _, _, err := m.Join(context.Background(), fresh, gc, gc.Player1ID); err != nil {
|
||||
@@ -253,6 +255,195 @@ func TestAckThenReconnectResendsCurrentState(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerStateTransitions(t *testing.T) {
|
||||
m := NewManager(time.Minute)
|
||||
gc := claims(time.Now().Add(time.Hour))
|
||||
c1, c2 := &fakeConn{}, &fakeConn{}
|
||||
|
||||
match, _, err := m.Join(context.Background(), c1, gc, gc.Player1ID)
|
||||
if err != nil {
|
||||
t.Fatalf("join p1: %v", err)
|
||||
}
|
||||
if _, _, err := m.Join(context.Background(), c2, gc, gc.Player2ID); err != nil {
|
||||
t.Fatalf("join p2: %v", err)
|
||||
}
|
||||
// Initial state is StatePlacingShips
|
||||
if match.State() != StatePlacingShips {
|
||||
t.Fatalf("state = %d, want StatePlacingShips", match.State())
|
||||
}
|
||||
|
||||
// Move to StatePlayer1Ready
|
||||
m.SetState(context.Background(), match, StatePlayer1Ready)
|
||||
p1Msgs, p2Msgs := c1.messages(), c2.messages()
|
||||
if v := parseMsg(t, p1Msgs[len(p1Msgs)-1]); v[2] != float64(protocol.StateWaitOpponentReady) {
|
||||
t.Fatalf("p1 got %v, want StateWaitOpponentReady (3)", v)
|
||||
}
|
||||
if v := parseMsg(t, p2Msgs[len(p2Msgs)-1]); v[2] != float64(protocol.StateOpponentReady) {
|
||||
t.Fatalf("p2 got %v, want StateOpponentReady (2)", v)
|
||||
}
|
||||
|
||||
// Move to StatePlayer1Turn
|
||||
m.SetState(context.Background(), match, StatePlayer1Turn)
|
||||
p1Msgs, p2Msgs = c1.messages(), c2.messages()
|
||||
if v := parseMsg(t, p1Msgs[len(p1Msgs)-1]); v[2] != float64(protocol.StateYourTurn) {
|
||||
t.Fatalf("p1 got %v, want StateYourTurn (4)", v)
|
||||
}
|
||||
if v := parseMsg(t, p2Msgs[len(p2Msgs)-1]); v[2] != float64(protocol.StateWaitOpponentTurn) {
|
||||
t.Fatalf("p2 got %v, want StateWaitOpponentTurn (5)", v)
|
||||
}
|
||||
|
||||
// Move to StatePlayer2Turn
|
||||
m.SetState(context.Background(), match, StatePlayer2Turn)
|
||||
p1Msgs, p2Msgs = c1.messages(), c2.messages()
|
||||
if v := parseMsg(t, p1Msgs[len(p1Msgs)-1]); v[2] != float64(protocol.StateWaitOpponentTurn) {
|
||||
t.Fatalf("p1 got %v, want StateWaitOpponentTurn (5)", v)
|
||||
}
|
||||
if v := parseMsg(t, p2Msgs[len(p2Msgs)-1]); v[2] != float64(protocol.StateYourTurn) {
|
||||
t.Fatalf("p2 got %v, want StateYourTurn (4)", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAckValidation(t *testing.T) {
|
||||
m := NewManager(time.Minute)
|
||||
gc := claims(time.Now().Add(time.Hour))
|
||||
c1, c2 := &fakeConn{}, &fakeConn{}
|
||||
|
||||
match, _, err := m.Join(context.Background(), c1, gc, gc.Player1ID)
|
||||
if err != nil {
|
||||
t.Fatalf("join p1: %v", err)
|
||||
}
|
||||
if _, _, err := m.Join(context.Background(), c2, gc, gc.Player2ID); err != nil {
|
||||
t.Fatalf("join p2: %v", err)
|
||||
}
|
||||
|
||||
// seq <= 0 should fail
|
||||
if err := m.Ack(match, 0, 0); err != ErrInvalidSeq {
|
||||
t.Fatalf("ack 0: got err = %v, want ErrInvalidSeq", err)
|
||||
}
|
||||
if err := m.Ack(match, 0, -1); err != ErrInvalidSeq {
|
||||
t.Fatalf("ack -1: got err = %v, want ErrInvalidSeq", err)
|
||||
}
|
||||
|
||||
// seq > slot.seq (e.g. 2 when only 1 sent) should fail
|
||||
if err := m.Ack(match, 0, 2); err != ErrInvalidSeq {
|
||||
t.Fatalf("ack 2: got err = %v, want ErrInvalidSeq", err)
|
||||
}
|
||||
|
||||
// seq == 1 should succeed
|
||||
if err := m.Ack(match, 0, 1); err != nil {
|
||||
t.Fatalf("ack 1: unexpected err = %v", err)
|
||||
}
|
||||
|
||||
// Idempotent duplicate ack should succeed
|
||||
if err := m.Ack(match, 0, 1); err != nil {
|
||||
t.Fatalf("repeat ack 1: unexpected err = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEndMatchCleansUpMatch(t *testing.T) {
|
||||
m := NewManager(time.Minute)
|
||||
gc := claims(time.Now().Add(time.Hour))
|
||||
c1, c2 := &fakeConn{}, &fakeConn{}
|
||||
|
||||
match, _, err := m.Join(context.Background(), c1, gc, gc.Player1ID)
|
||||
if err != nil {
|
||||
t.Fatalf("join p1: %v", err)
|
||||
}
|
||||
if _, _, err := m.Join(context.Background(), c2, gc, gc.Player2ID); err != nil {
|
||||
t.Fatalf("join p2: %v", err)
|
||||
}
|
||||
|
||||
// Match started successfully and is in m.matches
|
||||
if m.GetMatch(gc.MatchID) == nil {
|
||||
t.Fatal("expected match in m.matches")
|
||||
}
|
||||
|
||||
// End match
|
||||
m.EndMatch(match.ID, 0, "game completed")
|
||||
|
||||
// Match should be deleted from m.matches (preventing memory leak)
|
||||
if m.GetMatch(gc.MatchID) != nil {
|
||||
t.Fatal("expected match to be deleted from m.matches")
|
||||
}
|
||||
|
||||
// Both connections should be notified and closed with CloseCodeNormal
|
||||
for i, c := range []*fakeConn{c1, c2} {
|
||||
msgs := c.messages()
|
||||
lastMsg := parseMsg(t, msgs[len(msgs)-1])
|
||||
if lastMsg[0] != float64(protocol.OpEnd) || lastMsg[3] != "game completed" {
|
||||
t.Fatalf("player %d unexpected END: %v", i, lastMsg)
|
||||
}
|
||||
if closed, code := c.isClosed(); !closed || code != protocol.CloseCodeNormal {
|
||||
t.Fatalf("player %d closed=%v code=%d, want closed with %d", i, closed, code, protocol.CloseCodeNormal)
|
||||
}
|
||||
}
|
||||
|
||||
// Late join should replay END
|
||||
c3 := &fakeConn{}
|
||||
_, _, err = m.Join(context.Background(), c3, gc, gc.Player1ID)
|
||||
if err != ErrMatchFinished {
|
||||
t.Fatalf("err = %v, want ErrMatchFinished", err)
|
||||
}
|
||||
msgs := c3.messages()
|
||||
if len(msgs) != 1 {
|
||||
t.Fatalf("replayed %d messages, want 1", len(msgs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerShutdown(t *testing.T) {
|
||||
m := NewManager(time.Minute)
|
||||
gc := claims(time.Now().Add(time.Hour))
|
||||
c1, c2 := &fakeConn{}, &fakeConn{}
|
||||
|
||||
_, _, err := m.Join(context.Background(), c1, gc, gc.Player1ID)
|
||||
if err != nil {
|
||||
t.Fatalf("join p1: %v", err)
|
||||
}
|
||||
if _, _, err := m.Join(context.Background(), c2, gc, gc.Player2ID); err != nil {
|
||||
t.Fatalf("join p2: %v", err)
|
||||
}
|
||||
|
||||
m.Shutdown()
|
||||
|
||||
for i, c := range []*fakeConn{c1, c2} {
|
||||
if closed, code := c.isClosed(); !closed || code != protocol.CloseCodeGoingAway {
|
||||
t.Fatalf("player %d closed=%v code=%d, want closed with %d", i, closed, code, protocol.CloseCodeGoingAway)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJoinWhenMatchAlreadyEndedReplaysReason(t *testing.T) {
|
||||
m := NewManager(time.Minute)
|
||||
gc := claims(time.Now().Add(time.Hour))
|
||||
c1 := &fakeConn{}
|
||||
|
||||
_, _, err := m.Join(context.Background(), c1, gc, gc.Player1ID)
|
||||
if err != nil {
|
||||
t.Fatalf("join: %v", err)
|
||||
}
|
||||
|
||||
// Trigger onJoinTimeout so match is ended
|
||||
m.onJoinTimeout(gc.MatchID)
|
||||
|
||||
// Late join on ended match must send the reason before closing
|
||||
c2 := &fakeConn{}
|
||||
_, _, err = m.Join(context.Background(), c2, gc, gc.Player2ID)
|
||||
if err != ErrMatchFinished {
|
||||
t.Fatalf("got err = %v, want ErrMatchFinished", err)
|
||||
}
|
||||
msgs := c2.messages()
|
||||
if len(msgs) != 1 {
|
||||
t.Fatalf("got %d messages, want 1", len(msgs))
|
||||
}
|
||||
v := parseMsg(t, msgs[0])
|
||||
if v[0] != float64(protocol.OpEnd) || v[2] != float64(protocol.EndCodeJoinTimeout) || v[3] != "join timeout exceeded" {
|
||||
t.Fatalf("unexpected message: %v", v)
|
||||
}
|
||||
if closed, code := c2.isClosed(); !closed || code != protocol.CloseCodeNormal {
|
||||
t.Fatalf("c2 closed=%v code=%d, want %d", closed, code, protocol.CloseCodeNormal)
|
||||
}
|
||||
}
|
||||
|
||||
func mustMatch(t *testing.T, m *Manager, id string) *Match {
|
||||
t.Helper()
|
||||
m.mu.Lock()
|
||||
|
||||
Reference in New Issue
Block a user