mirror of
https://github.com/codecat/go-enet.git
synced 2025-12-08 05:59:47 +00:00
Decided to allow storing of arbitrary data saved as a byte slice, leaving the conversion of whatever data that wants to be stored (e.g. an identified or JSON) up to the consumer. Importantly this data is being stored in enet C field so we ensure that the data is being copied out in to C allocation, rather than allowing C code to have Go pointers. For this reason, it is up to the caller to unset data when the peer is done with to avoid memory leaks. I couldn't find a way with enet to have this automatically cleaned up.
209 lines
5.5 KiB
Go
209 lines
5.5 KiB
Go
package enet_test
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/codecat/go-enet"
|
|
"os"
|
|
"os/exec"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestPeerData(t *testing.T) {
|
|
testData := []byte{0x1, 0x2, 0x3}
|
|
|
|
// peer is connected to our server.
|
|
// events will produce events as the server receives them.
|
|
peer, events := createServerClient(t)
|
|
|
|
// Wait for the server to respond with a connection.
|
|
ev := <-events
|
|
if data := ev.GetPeer().GetData(); data != nil {
|
|
t.Fatalf("did not expect new peer to have data set, but has %x", data)
|
|
}
|
|
|
|
// Set some data against our peer and immediately check it's there.
|
|
ev.GetPeer().SetData(testData)
|
|
assertPeerData(t, ev.GetPeer(), testData, "immediate after set")
|
|
|
|
// Send a message to the server.
|
|
if err := peer.SendString("testmessage", 0, enet.PacketFlagReliable); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Wait for the server to receive this message, then check the
|
|
// server-side peer associated with this event has the data
|
|
// we set previously.
|
|
ev = <-events
|
|
assertPeerData(t, ev.GetPeer(), testData, "on packet received")
|
|
|
|
t.Run("clear-data", func(t *testing.T) {
|
|
ev.GetPeer().SetData(nil)
|
|
assertPeerData(t, ev.GetPeer(), nil, "nil set")
|
|
})
|
|
|
|
t.Run("empty-slice", func(t *testing.T) {
|
|
ev.GetPeer().SetData([]byte{})
|
|
assertPeerData(t, ev.GetPeer(), []byte{}, "empty set")
|
|
})
|
|
|
|
// Check that our data stored in C survives garbage collection.
|
|
t.Run("survives-gc", func(t *testing.T) {
|
|
ev.GetPeer().SetData([]byte{1, 2, 3})
|
|
runtime.GC()
|
|
assertPeerData(t, ev.GetPeer(), []byte{1, 2, 3}, "after GC")
|
|
})
|
|
|
|
// Sniffs for a potential memory leak in our set data implementation.
|
|
// We expect SetData to clear whatever C memory was used previously.
|
|
// This may end up being a flaky test, but will keep it in for now to
|
|
// build confidence in our implementation.
|
|
t.Run("memory-leaks", func(t *testing.T) {
|
|
if runtime.GOOS != "linux" {
|
|
t.Skipf("skipping mem leak test as not running on a linux host")
|
|
}
|
|
|
|
noOfIncreases := 0
|
|
last := currentMemory(t)
|
|
|
|
// Assign a large string (10MB) to data over and over again, checking
|
|
// for continuous increases in mem usage, with some threshold.
|
|
for i := 0; i < 99; i++ {
|
|
ev.GetPeer().SetData([]byte(strings.Repeat("x", 1024*1024*10)))
|
|
|
|
// Detect a memory leak by checking if we're using more than 1MB last than the
|
|
// previous for too many iterations.
|
|
now := currentMemory(t)
|
|
|
|
if now-last > 1024 {
|
|
noOfIncreases++
|
|
} else {
|
|
// If it's not an increase, reset our counter.
|
|
noOfIncreases = 0
|
|
}
|
|
|
|
// If we reach a threshold of 5 continuous increases, consider this a leak.
|
|
if noOfIncreases > 5 {
|
|
t.Fatal("potential memory leak detected")
|
|
}
|
|
|
|
last = now
|
|
}
|
|
})
|
|
}
|
|
|
|
func assertPeerData(t testing.TB, peer enet.Peer, expected []byte, msg string) {
|
|
t.Helper()
|
|
|
|
actual := peer.GetData()
|
|
|
|
if (actual == nil) != (expected == nil) {
|
|
t.Fatalf("%s: expected peer data to be present? %t vs actual: %t", msg, expected != nil, actual != nil)
|
|
}
|
|
|
|
if len(actual) != len(expected) {
|
|
t.Fatalf("%s: expected peer data to have len %d vs actual %d", msg, len(expected), len(actual))
|
|
}
|
|
|
|
if string(actual) != string(expected) {
|
|
t.Fatalf("%s: expected peer data to be %v, but it was %v", msg, expected, actual)
|
|
}
|
|
}
|
|
|
|
// createServerClient creates a dummy enet server and client. The returned
|
|
// peer can be used to send messages to the server, and the blocking events
|
|
// channel returned will be given each event as the server picks it up.
|
|
func createServerClient(t *testing.T) (clientConn enet.Peer, serverEvents <-chan enet.Event) {
|
|
port := getFreePort()
|
|
|
|
done := make(chan bool, 0)
|
|
events := make(chan enet.Event)
|
|
|
|
t.Cleanup(func() {
|
|
// Kill our background service routines for client & server.
|
|
close(done)
|
|
})
|
|
|
|
// Create a server and continuously service it, exposing any captured events.
|
|
server, err := enet.NewHost(enet.NewListenAddress(port), 10, 1, 0, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
go func() {
|
|
for true {
|
|
select {
|
|
case <-done:
|
|
return
|
|
default:
|
|
ev := server.Service(0)
|
|
|
|
// Pass any event out to our channel. This will block
|
|
// until a test consumes it.
|
|
if ev.GetType() != enet.EventNone {
|
|
events <- ev
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Create a client and continuously service it in the background.
|
|
client, err := enet.NewHost(nil, 1, 1, 0, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
go func() {
|
|
for true {
|
|
select {
|
|
case <-done:
|
|
return
|
|
default:
|
|
client.Service(0)
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Connect to our server.
|
|
peer, err := client.Connect(enet.NewAddress("localhost", port), 1, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
return peer, events
|
|
}
|
|
|
|
var port uint16 = 49152
|
|
|
|
// getFreePort returns a unique private port. Note this doesn't guarantee
|
|
// it's free, but should be good enough from within docker tests.
|
|
func getFreePort() uint16 {
|
|
port++
|
|
return port
|
|
}
|
|
|
|
// currentMemory returns the memory usage of the current process according to
|
|
// the OS. This uses linux's proc FS to give a rough estimate based on VmSize.
|
|
// Note we don't want to use runtime.MemStats here as we're looking for the
|
|
// total memory (including C allocations).
|
|
func currentMemory(t testing.TB) int {
|
|
// GC to give a stable measure.
|
|
runtime.GC()
|
|
|
|
pmapCmd := fmt.Sprintf("pmap %d | grep total | grep -Eo '[0-9]+'", os.Getpid())
|
|
cmd := exec.Command("bash", "-c", pmapCmd)
|
|
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
t.Fatalf("failed to run memory check command: %s", err)
|
|
}
|
|
|
|
kb, err := strconv.Atoi(strings.TrimSpace(string(output)))
|
|
if err != nil {
|
|
t.Fatalf("failed converting memory output provided by pmap to int: %s", err)
|
|
}
|
|
|
|
return kb
|
|
}
|