From ea373ed078d3675bdee07c18ccb1ff206d680c67 Mon Sep 17 00:00:00 2001 From: Mark Mandel Date: Wed, 13 May 2020 11:19:52 -0700 Subject: [PATCH 1/2] E2E Tests for GameServer Player Tracking Updated the udp-simple example to accept various player tracking commands, and also implemented e2e tests to test it working on a singular GameServer instance. Work on #1507 --- build/Makefile | 2 +- examples/simple-udp/Makefile | 2 +- examples/simple-udp/main.go | 113 +++++++++++++++++++++++ test/e2e/framework/framework.go | 2 +- test/e2e/gameserver_test.go | 158 ++++++++++++++++++++++++++++++++ 5 files changed, 274 insertions(+), 3 deletions(-) diff --git a/build/Makefile b/build/Makefile index 098b205145..e8f6039cb0 100644 --- a/build/Makefile +++ b/build/Makefile @@ -57,7 +57,7 @@ KIND_PROFILE ?= agones KIND_CONTAINER_NAME=$(KIND_PROFILE)-control-plane # Game Server image to use while doing end-to-end tests -GS_TEST_IMAGE ?= gcr.io/agones-images/udp-server:0.19 +GS_TEST_IMAGE ?= gcr.io/agones-images/udp-server:0.20 ALL_FEATURE_GATES ?= "PlayerTracking=true&ContainerPortAllocation=true" diff --git a/examples/simple-udp/Makefile b/examples/simple-udp/Makefile index 53feca18c4..c2c9e99332 100644 --- a/examples/simple-udp/Makefile +++ b/examples/simple-udp/Makefile @@ -27,7 +27,7 @@ REPOSITORY = gcr.io/agones-images mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST))) project_path := $(dir $(mkfile_path)) -server_tag = $(REPOSITORY)/udp-server:0.19 +server_tag = $(REPOSITORY)/udp-server:0.20 root_path = $(realpath $(project_path)/../..) # _____ _ diff --git a/examples/simple-udp/main.go b/examples/simple-udp/main.go index b56112d4bb..81d96653d9 100644 --- a/examples/simple-udp/main.go +++ b/examples/simple-udp/main.go @@ -176,6 +176,52 @@ func readWriteLoop(conn net.PacketConn, stop chan struct{}, s *sdk.SDK) { respond(conn, sender, "ERROR: Invalid ANNOTATION command, must use zero or 2 arguments\n") continue } + case "PLAYER_CAPACITY": + switch len(parts) { + case 1: + respond(conn, sender, getPlayerCapacity(s)) + continue + case 2: + if cap, err := strconv.Atoi(parts[1]); err != nil { + respond(conn, sender, err.Error()+"\n") + continue + } else { + setPlayerCapacity(s, int64(cap)) + } + default: + respond(conn, sender, "ERROR: Invalid PLAYER_CAPACITY, should have 0 or 1 arguments\n") + continue + } + + case "PLAYER_CONNECT": + if len(parts) < 2 { + respond(conn, sender, "ERROR: Invalid PLAYER_CONNECT, should have 1 arguments\n") + continue + } + playerConnect(s, parts[1]) + + case "PLAYER_DISCONNECT": + if len(parts) < 2 { + respond(conn, sender, "ERROR: Invalid PLAYER_CONNECT, should have 1 arguments\n") + continue + } + playerDisconnect(s, parts[1]) + + case "PLAYER_CONNECTED": + if len(parts) < 2 { + respond(conn, sender, "ERROR: Invalid PLAYER_CONNECTED, should have 1 arguments\n") + continue + } + respond(conn, sender, playerIsConnected(s, parts[1])) + continue + + case "GET_PLAYERS": + respond(conn, sender, getConnectedPlayers(s)) + continue + + case "PLAYER_COUNT": + respond(conn, sender, getPlayerCount(s)) + continue } respond(conn, sender, "ACK: "+txt+"\n") @@ -284,6 +330,73 @@ func setLabel(s *sdk.SDK, key, value string) { } } +// setPlayerCapacity sets the player capacity to the given value +func setPlayerCapacity(s *sdk.SDK, capacity int64) { + log.Printf("Setting Player Capacity to %d", capacity) + if err := s.Alpha().SetPlayerCapacity(capacity); err != nil { + log.Fatalf("could not set capacity: %v", err) + } +} + +// getPlayerCapacity returns the current player capacity as a string +func getPlayerCapacity(s *sdk.SDK) string { + log.Print("Getting Player Capacity") + capacity, err := s.Alpha().GetPlayerCapacity() + if err != nil { + log.Fatalf("could not get capacity: %v", err) + } + return strconv.FormatInt(capacity, 10) +} + +// playerConnect connects a given player +func playerConnect(s *sdk.SDK, id string) { + log.Printf("Connecting Player: %s", id) + if _, err := s.Alpha().PlayerConnect(id); err != nil { + log.Fatalf("could not connect player: %v", err) + } +} + +// playerDisconnect disconnects a given player +func playerDisconnect(s *sdk.SDK, id string) { + log.Printf("Disconnecting Player: %s", id) + if _, err := s.Alpha().PlayerDisconnect(id); err != nil { + log.Fatalf("could not disconnect player: %v", err) + } +} + +// playerIsConnected returns a bool as a string if a player is connected +func playerIsConnected(s *sdk.SDK, id string) string { + log.Printf("Checking if player %s is connected", id) + + connected, err := s.Alpha().IsPlayerConnected(id) + if err != nil { + log.Fatalf("could not retrieve if player is connected: %v", err) + } + + return strconv.FormatBool(connected) +} + +// getConnectedPlayers returns a comma delimeted list of connected players +func getConnectedPlayers(s *sdk.SDK) string { + log.Print("Retrieving connected player list") + list, err := s.Alpha().GetConnectedPlayers() + if err != nil { + log.Fatalf("could not retrieve connected players: %s", err) + } + + return strings.Join(list, ",") +} + +// getPlayerCount returns the count of connected players as a string +func getPlayerCount(s *sdk.SDK) string { + log.Print("Retrieving connected player count") + count, err := s.Alpha().GetPlayerCount() + if err != nil { + log.Fatalf("could not retrieve player count: %s", err) + } + return strconv.FormatInt(count, 10) +} + // doHealth sends the regular Health Pings func doHealth(sdk *sdk.SDK, stop <-chan struct{}) { tick := time.Tick(2 * time.Second) diff --git a/test/e2e/framework/framework.go b/test/e2e/framework/framework.go index 1a2446007b..e9853ac1d8 100644 --- a/test/e2e/framework/framework.go +++ b/test/e2e/framework/framework.go @@ -147,7 +147,7 @@ func NewFromFlags() (*Framework, error) { } viper.SetDefault(kubeconfigFlag, filepath.Join(usr.HomeDir, "/.kube/config")) - viper.SetDefault(gsimageFlag, "gcr.io/agones-images/udp-server:0.19") + viper.SetDefault(gsimageFlag, "gcr.io/agones-images/udp-server:0.20") viper.SetDefault(pullSecretFlag, "") viper.SetDefault(stressTestLevelFlag, 0) viper.SetDefault(perfOutputDirFlag, "") diff --git a/test/e2e/gameserver_test.go b/test/e2e/gameserver_test.go index 40df70ce9d..2714ce6910 100644 --- a/test/e2e/gameserver_test.go +++ b/test/e2e/gameserver_test.go @@ -736,3 +736,161 @@ func TestGameServerResourceValidation(t *testing.T) { assert.Equal(t, metav1.CauseTypeFieldValueInvalid, statusErr.Status().Details.Causes[0].Type) assert.Equal(t, "container", statusErr.Status().Details.Causes[0].Field) } + +func TestGameServerSetPlayerCapacity(t *testing.T) { + if !runtime.FeatureEnabled(runtime.FeaturePlayerTracking) { + t.SkipNow() + } + t.Parallel() + + t.Run("no initial capacity set", func(t *testing.T) { + gs := framework.DefaultGameServer(defaultNs) + gs, err := framework.CreateGameServerAndWaitUntilReady(defaultNs, gs) + if err != nil { + t.Fatalf("Could not get a GameServer ready: %v", err) + } + assert.Equal(t, gs.Status.State, agonesv1.GameServerStateReady) + assert.Equal(t, int64(0), gs.Status.Players.Capacity) + + reply, err := e2eframework.SendGameServerUDP(gs, "PLAYER_CAPACITY") + assert.NoError(t, err) + assert.Equal(t, "0", reply) + + reply, err = e2eframework.SendGameServerUDP(gs, "PLAYER_CAPACITY 20") + if err != nil { + t.Fatalf("Could not message GameServer: %v", err) + } + assert.Equal(t, "ACK: PLAYER_CAPACITY 20\n", reply) + + reply, err = e2eframework.SendGameServerUDP(gs, "PLAYER_CAPACITY") + assert.NoError(t, err) + assert.Equal(t, "20", reply) + + err = wait.PollImmediate(time.Second, time.Minute, func() (bool, error) { + gs, err := framework.AgonesClient.AgonesV1().GameServers(defaultNs).Get(gs.ObjectMeta.Name, metav1.GetOptions{}) + if err != nil { + return false, err + } + return gs.Status.Players.Capacity == 20, nil + }) + assert.NoError(t, err) + }) + + t.Run("initial capacity set", func(t *testing.T) { + gs := framework.DefaultGameServer(defaultNs) + gs.Spec.Players = &agonesv1.PlayersSpec{InitialCapacity: 10} + gs, err := framework.CreateGameServerAndWaitUntilReady(defaultNs, gs) + if err != nil { + t.Fatalf("Could not get a GameServer ready: %v", err) + } + assert.Equal(t, gs.Status.State, agonesv1.GameServerStateReady) + assert.Equal(t, int64(10), gs.Status.Players.Capacity) + + reply, err := e2eframework.SendGameServerUDP(gs, "PLAYER_CAPACITY") + assert.NoError(t, err) + assert.Equal(t, "10", reply) + + reply, err = e2eframework.SendGameServerUDP(gs, "PLAYER_CAPACITY 20") + if err != nil { + t.Fatalf("Could not message GameServer: %v", err) + } + assert.Equal(t, "ACK: PLAYER_CAPACITY 20\n", reply) + + reply, err = e2eframework.SendGameServerUDP(gs, "PLAYER_CAPACITY") + assert.NoError(t, err) + assert.Equal(t, "20", reply) + + err = wait.PollImmediate(time.Second, time.Minute, func() (bool, error) { + gs, err := framework.AgonesClient.AgonesV1().GameServers(defaultNs).Get(gs.ObjectMeta.Name, metav1.GetOptions{}) + if err != nil { + return false, err + } + return gs.Status.Players.Capacity == 20, nil + }) + assert.NoError(t, err) + + time.Sleep(30 * time.Second) + }) +} + +func TestPlayerConnectAndDisconnect(t *testing.T) { + if !runtime.FeatureEnabled(runtime.FeaturePlayerTracking) { + t.SkipNow() + } + t.Parallel() + + gs := framework.DefaultGameServer(defaultNs) + playerCount := int64(3) + gs.Spec.Players = &agonesv1.PlayersSpec{InitialCapacity: playerCount} + gs, err := framework.CreateGameServerAndWaitUntilReady(defaultNs, gs) + if err != nil { + t.Fatalf("Could not get a GameServer ready: %v", err) + } + assert.Equal(t, gs.Status.State, agonesv1.GameServerStateReady) + assert.Equal(t, playerCount, gs.Status.Players.Capacity) + + // add three players in quick succession + for i := int64(1); i <= playerCount; i++ { + msg := "PLAYER_CONNECT " + fmt.Sprintf("%d", i) + logrus.WithField("msg", msg).Info("Sending Player Connect") + reply, err := e2eframework.SendGameServerUDP(gs, msg) + if err != nil { + t.Fatalf("Could not message GameServer: %v", err) + } + assert.Equal(t, fmt.Sprintf("ACK: %s\n", msg), reply) + } + + // deliberately do this before polling, to test the SDK returning the correct + // results before it is committed to the GameServer resource. + reply, err := e2eframework.SendGameServerUDP(gs, "PLAYER_CONNECTED 1") + assert.NoError(t, err) + assert.Equal(t, "true", reply) + + reply, err = e2eframework.SendGameServerUDP(gs, "GET_PLAYERS") + assert.NoError(t, err) + assert.ElementsMatch(t, []string{"1", "2", "3"}, strings.Split(reply, ",")) + + reply, err = e2eframework.SendGameServerUDP(gs, "PLAYER_COUNT") + assert.NoError(t, err) + assert.Equal(t, "3", reply) + + err = wait.Poll(time.Second, time.Minute, func() (bool, error) { + gs, err = framework.AgonesClient.AgonesV1().GameServers(defaultNs).Get(gs.ObjectMeta.Name, metav1.GetOptions{}) + if err != nil { + return false, err + } + return gs.Status.Players.Count == playerCount, nil + }) + assert.NoError(t, err) + assert.ElementsMatch(t, []string{"1", "2", "3"}, gs.Status.Players.IDs) + + // let's disconnect player 2 + logrus.Info("Disconnect Player 2") + reply, err = e2eframework.SendGameServerUDP(gs, "PLAYER_DISCONNECT 2") + if err != nil { + t.Fatalf("Could not message GameServer: %v", err) + } + assert.Equal(t, "ACK: PLAYER_DISCONNECT 2\n", reply) + + reply, err = e2eframework.SendGameServerUDP(gs, "PLAYER_CONNECTED 2") + assert.NoError(t, err) + assert.Equal(t, "false", reply) + + reply, err = e2eframework.SendGameServerUDP(gs, "GET_PLAYERS") + assert.NoError(t, err) + assert.ElementsMatch(t, []string{"1", "3"}, strings.Split(reply, ",")) + + reply, err = e2eframework.SendGameServerUDP(gs, "PLAYER_COUNT") + assert.NoError(t, err) + assert.Equal(t, "2", reply) + + err = wait.Poll(time.Second, time.Minute, func() (bool, error) { + gs, err = framework.AgonesClient.AgonesV1().GameServers(defaultNs).Get(gs.ObjectMeta.Name, metav1.GetOptions{}) + if err != nil { + return false, err + } + return gs.Status.Players.Count == 2, nil + }) + assert.NoError(t, err) + assert.ElementsMatch(t, []string{"1", "3"}, gs.Status.Players.IDs) +} From 193bfe6e9a2ea2e952000b3bb624fb32cbba763a Mon Sep 17 00:00:00 2001 From: Mark Mandel Date: Wed, 13 May 2020 14:24:59 -0700 Subject: [PATCH 2/2] Updates from review. --- examples/simple-udp/main.go | 8 ++++---- test/e2e/framework/framework.go | 2 +- test/e2e/gameserver_test.go | 20 ++++++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/examples/simple-udp/main.go b/examples/simple-udp/main.go index 81d96653d9..25f1a20bed 100644 --- a/examples/simple-udp/main.go +++ b/examples/simple-udp/main.go @@ -345,7 +345,7 @@ func getPlayerCapacity(s *sdk.SDK) string { if err != nil { log.Fatalf("could not get capacity: %v", err) } - return strconv.FormatInt(capacity, 10) + return strconv.FormatInt(capacity, 10) + "\n" } // playerConnect connects a given player @@ -373,7 +373,7 @@ func playerIsConnected(s *sdk.SDK, id string) string { log.Fatalf("could not retrieve if player is connected: %v", err) } - return strconv.FormatBool(connected) + return strconv.FormatBool(connected) + "\n" } // getConnectedPlayers returns a comma delimeted list of connected players @@ -384,7 +384,7 @@ func getConnectedPlayers(s *sdk.SDK) string { log.Fatalf("could not retrieve connected players: %s", err) } - return strings.Join(list, ",") + return strings.Join(list, ",") + "\n" } // getPlayerCount returns the count of connected players as a string @@ -394,7 +394,7 @@ func getPlayerCount(s *sdk.SDK) string { if err != nil { log.Fatalf("could not retrieve player count: %s", err) } - return strconv.FormatInt(count, 10) + return strconv.FormatInt(count, 10) + "\n" } // doHealth sends the regular Health Pings diff --git a/test/e2e/framework/framework.go b/test/e2e/framework/framework.go index e9853ac1d8..494f8e37a9 100644 --- a/test/e2e/framework/framework.go +++ b/test/e2e/framework/framework.go @@ -155,7 +155,7 @@ func NewFromFlags() (*Framework, error) { viper.SetDefault(runtime.FeatureGateFlag, "") kubeconfig := pflag.String(kubeconfigFlag, viper.GetString(kubeconfigFlag), "kube config path, e.g. $HOME/.kube/config") - gsimage := pflag.String(gsimageFlag, viper.GetString(gsimageFlag), "gameserver image to use for those tests, gcr.io/agones-images/udp-server:0.19") + gsimage := pflag.String(gsimageFlag, viper.GetString(gsimageFlag), "gameserver image to use for those tests, gcr.io/agones-images/udp-server:0.20") pullSecret := pflag.String(pullSecretFlag, viper.GetString(pullSecretFlag), "optional secret to be used for pulling the gameserver and/or Agones SDK sidecar images") stressTestLevel := pflag.Int(stressTestLevelFlag, viper.GetInt(stressTestLevelFlag), "enable stress test at given level 0-100") perfOutputDir := pflag.String(perfOutputDirFlag, viper.GetString(perfOutputDirFlag), "write performance statistics to the specified directory") diff --git a/test/e2e/gameserver_test.go b/test/e2e/gameserver_test.go index 2714ce6910..cc5e5f5ea7 100644 --- a/test/e2e/gameserver_test.go +++ b/test/e2e/gameserver_test.go @@ -754,7 +754,7 @@ func TestGameServerSetPlayerCapacity(t *testing.T) { reply, err := e2eframework.SendGameServerUDP(gs, "PLAYER_CAPACITY") assert.NoError(t, err) - assert.Equal(t, "0", reply) + assert.Equal(t, "0\n", reply) reply, err = e2eframework.SendGameServerUDP(gs, "PLAYER_CAPACITY 20") if err != nil { @@ -764,7 +764,7 @@ func TestGameServerSetPlayerCapacity(t *testing.T) { reply, err = e2eframework.SendGameServerUDP(gs, "PLAYER_CAPACITY") assert.NoError(t, err) - assert.Equal(t, "20", reply) + assert.Equal(t, "20\n", reply) err = wait.PollImmediate(time.Second, time.Minute, func() (bool, error) { gs, err := framework.AgonesClient.AgonesV1().GameServers(defaultNs).Get(gs.ObjectMeta.Name, metav1.GetOptions{}) @@ -788,7 +788,7 @@ func TestGameServerSetPlayerCapacity(t *testing.T) { reply, err := e2eframework.SendGameServerUDP(gs, "PLAYER_CAPACITY") assert.NoError(t, err) - assert.Equal(t, "10", reply) + assert.Equal(t, "10\n", reply) reply, err = e2eframework.SendGameServerUDP(gs, "PLAYER_CAPACITY 20") if err != nil { @@ -798,7 +798,7 @@ func TestGameServerSetPlayerCapacity(t *testing.T) { reply, err = e2eframework.SendGameServerUDP(gs, "PLAYER_CAPACITY") assert.NoError(t, err) - assert.Equal(t, "20", reply) + assert.Equal(t, "20\n", reply) err = wait.PollImmediate(time.Second, time.Minute, func() (bool, error) { gs, err := framework.AgonesClient.AgonesV1().GameServers(defaultNs).Get(gs.ObjectMeta.Name, metav1.GetOptions{}) @@ -844,15 +844,15 @@ func TestPlayerConnectAndDisconnect(t *testing.T) { // results before it is committed to the GameServer resource. reply, err := e2eframework.SendGameServerUDP(gs, "PLAYER_CONNECTED 1") assert.NoError(t, err) - assert.Equal(t, "true", reply) + assert.Equal(t, "true\n", reply) reply, err = e2eframework.SendGameServerUDP(gs, "GET_PLAYERS") assert.NoError(t, err) - assert.ElementsMatch(t, []string{"1", "2", "3"}, strings.Split(reply, ",")) + assert.ElementsMatch(t, []string{"1", "2", "3"}, strings.Split(strings.TrimSpace(reply), ",")) reply, err = e2eframework.SendGameServerUDP(gs, "PLAYER_COUNT") assert.NoError(t, err) - assert.Equal(t, "3", reply) + assert.Equal(t, "3\n", reply) err = wait.Poll(time.Second, time.Minute, func() (bool, error) { gs, err = framework.AgonesClient.AgonesV1().GameServers(defaultNs).Get(gs.ObjectMeta.Name, metav1.GetOptions{}) @@ -874,15 +874,15 @@ func TestPlayerConnectAndDisconnect(t *testing.T) { reply, err = e2eframework.SendGameServerUDP(gs, "PLAYER_CONNECTED 2") assert.NoError(t, err) - assert.Equal(t, "false", reply) + assert.Equal(t, "false\n", reply) reply, err = e2eframework.SendGameServerUDP(gs, "GET_PLAYERS") assert.NoError(t, err) - assert.ElementsMatch(t, []string{"1", "3"}, strings.Split(reply, ",")) + assert.ElementsMatch(t, []string{"1", "3"}, strings.Split(strings.TrimSpace(reply), ",")) reply, err = e2eframework.SendGameServerUDP(gs, "PLAYER_COUNT") assert.NoError(t, err) - assert.Equal(t, "2", reply) + assert.Equal(t, "2\n", reply) err = wait.Poll(time.Second, time.Minute, func() (bool, error) { gs, err = framework.AgonesClient.AgonesV1().GameServers(defaultNs).Get(gs.ObjectMeta.Name, metav1.GetOptions{})