initial commit
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
// Package e2e contains end-to-end integration tests that exercise the full
|
||||
// redapricot data path: a real Java hub subprocess, the in-process Go client,
|
||||
// a mock Minecraft destination, and simulated players. The tests live in
|
||||
// *_test.go files; this file exists so `go build ./...` has a buildable package.
|
||||
package e2e
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/iceBear67/redapricot/client"
|
||||
)
|
||||
|
||||
// startClient builds and starts an in-process client against the hub, with the
|
||||
// given mappings, returning the running client.
|
||||
func startClient(t *testing.T, hubAddr, psk string, maxConn int, mappings []client.Mapping) *client.Client {
|
||||
t.Helper()
|
||||
cfg := &client.Config{
|
||||
Server: hubAddr,
|
||||
PSK: psk,
|
||||
MaxConn: maxConn,
|
||||
PingIntervalMs: 20000,
|
||||
Mappings: mappings,
|
||||
}
|
||||
c := client.New(cfg)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(func() {
|
||||
cancel()
|
||||
c.Close()
|
||||
})
|
||||
if err := c.Start(ctx); err != nil {
|
||||
t.Fatalf("client start: %v", err)
|
||||
}
|
||||
// Give the control-session registration a moment to reach the hub.
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
return c
|
||||
}
|
||||
|
||||
// TestRoundTrip verifies the full path plus verbatim handshake forwarding and
|
||||
// case-insensitive pattern matching.
|
||||
func TestRoundTrip(t *testing.T) {
|
||||
const psk = "e2e-roundtrip"
|
||||
port := freePort(t)
|
||||
hubAddr := fmt.Sprintf("127.0.0.1:%d", port)
|
||||
startHub(t, port, psk)
|
||||
dest := newMockDest(t, modeEcho)
|
||||
startClient(t, hubAddr, psk, 4, []client.Mapping{
|
||||
{Pattern: "mc.local", Destination: dest.addr},
|
||||
})
|
||||
|
||||
// The player connects using mixed case; the hub matches case-insensitively
|
||||
// but forwards the address verbatim.
|
||||
pc := dialPlayer(t, hubAddr, "MC.Local")
|
||||
defer pc.Close()
|
||||
playerEcho(t, pc, []byte("hello redapricot"))
|
||||
|
||||
ev := dest.waitEvent(t, 5*time.Second)
|
||||
if ev.handshakeAddr != "MC.Local" {
|
||||
t.Fatalf("destination saw handshake address %q, want verbatim %q", ev.handshakeAddr, "MC.Local")
|
||||
}
|
||||
if ev.hasProxy {
|
||||
t.Fatalf("did not expect a PROXY header for a non-proxy mapping")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLargeTransfer pushes a multi-megabyte payload both ways to exercise mux
|
||||
// framing and back-pressure.
|
||||
func TestLargeTransfer(t *testing.T) {
|
||||
const psk = "e2e-large"
|
||||
port := freePort(t)
|
||||
hubAddr := fmt.Sprintf("127.0.0.1:%d", port)
|
||||
startHub(t, port, psk)
|
||||
dest := newMockDest(t, modeEcho)
|
||||
startClient(t, hubAddr, psk, 2, []client.Mapping{
|
||||
{Pattern: "mc.local", Destination: dest.addr},
|
||||
})
|
||||
|
||||
pc := dialPlayer(t, hubAddr, "mc.local")
|
||||
defer pc.Close()
|
||||
|
||||
payload := make([]byte, 3*1024*1024)
|
||||
for i := range payload {
|
||||
payload[i] = byte(i*31 + 7)
|
||||
}
|
||||
// Write from a goroutine while reading back concurrently to avoid deadlock.
|
||||
writeErr := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := pc.Write(payload)
|
||||
writeErr <- err
|
||||
}()
|
||||
got := make([]byte, len(payload))
|
||||
_ = pc.SetReadDeadline(time.Now().Add(30 * time.Second))
|
||||
if _, err := io.ReadFull(pc, got); err != nil {
|
||||
t.Fatalf("read large echo: %v", err)
|
||||
}
|
||||
if err := <-writeErr; err != nil {
|
||||
t.Fatalf("write large payload: %v", err)
|
||||
}
|
||||
for i := range payload {
|
||||
if got[i] != payload[i] {
|
||||
t.Fatalf("large echo mismatch at byte %d", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestConcurrentStreamsUseMultipleConns confirms the least-loaded allocator
|
||||
// opens additional worker connections once streams saturate (>8).
|
||||
func TestConcurrentStreamsUseMultipleConns(t *testing.T) {
|
||||
const psk = "e2e-concurrent"
|
||||
const n = 20
|
||||
const maxConn = 4
|
||||
port := freePort(t)
|
||||
hubAddr := fmt.Sprintf("127.0.0.1:%d", port)
|
||||
startHub(t, port, psk)
|
||||
dest := newMockDest(t, modeEcho)
|
||||
c := startClient(t, hubAddr, psk, maxConn, []client.Mapping{
|
||||
{Pattern: "mc.local", Destination: dest.addr},
|
||||
})
|
||||
|
||||
conns := make([]net.Conn, 0, n)
|
||||
defer func() {
|
||||
for _, pc := range conns {
|
||||
_ = pc.Close()
|
||||
}
|
||||
}()
|
||||
// Establish streams sequentially so allocation is deterministic; keep them
|
||||
// all open to hold streams active.
|
||||
for i := 0; i < n; i++ {
|
||||
pc := dialPlayer(t, hubAddr, "mc.local")
|
||||
playerEcho(t, pc, []byte(fmt.Sprintf("hello-%d", i)))
|
||||
conns = append(conns, pc)
|
||||
}
|
||||
|
||||
got := c.WorkerConnCount()
|
||||
if got < 2 {
|
||||
t.Fatalf("expected >=2 worker conns for %d concurrent streams, got %d", n, got)
|
||||
}
|
||||
if got > maxConn {
|
||||
t.Fatalf("worker conns %d exceed maxConn %d", got, maxConn)
|
||||
}
|
||||
t.Logf("%d concurrent streams spread over %d worker conn(s)", n, got)
|
||||
}
|
||||
|
||||
// TestProxyProtocol checks that the client prepends a correct HAProxy v2 header
|
||||
// carrying the player's real source address.
|
||||
func TestProxyProtocol(t *testing.T) {
|
||||
const psk = "e2e-proxy"
|
||||
port := freePort(t)
|
||||
hubAddr := fmt.Sprintf("127.0.0.1:%d", port)
|
||||
startHub(t, port, psk)
|
||||
dest := newMockDest(t, modeEcho)
|
||||
startClient(t, hubAddr, psk, 2, []client.Mapping{
|
||||
{Pattern: "mc.local", Destination: dest.addr, ProxyProtocol: true},
|
||||
})
|
||||
|
||||
pc := dialPlayer(t, hubAddr, "mc.local")
|
||||
defer pc.Close()
|
||||
playerEcho(t, pc, []byte("proxied hello"))
|
||||
|
||||
ev := dest.waitEvent(t, 5*time.Second)
|
||||
if !ev.hasProxy {
|
||||
t.Fatalf("expected a PROXY v2 header")
|
||||
}
|
||||
localPort := pc.LocalAddr().(*net.TCPAddr).Port
|
||||
if ev.proxy.srcPort != localPort {
|
||||
t.Fatalf("proxy src port %d, want player local port %d", ev.proxy.srcPort, localPort)
|
||||
}
|
||||
if !ev.proxy.srcIP.IsLoopback() {
|
||||
t.Fatalf("proxy src ip %v, want loopback", ev.proxy.srcIP)
|
||||
}
|
||||
t.Logf("PROXY v2: src=%v:%d dst=%v:%d", ev.proxy.srcIP, ev.proxy.srcPort, ev.proxy.dstIP, ev.proxy.dstPort)
|
||||
}
|
||||
|
||||
// TestPlayerDisconnectPropagates: player closing → hub FIN → destination EOF.
|
||||
func TestPlayerDisconnectPropagates(t *testing.T) {
|
||||
const psk = "e2e-disc"
|
||||
port := freePort(t)
|
||||
hubAddr := fmt.Sprintf("127.0.0.1:%d", port)
|
||||
startHub(t, port, psk)
|
||||
dest := newMockDest(t, modeEcho)
|
||||
startClient(t, hubAddr, psk, 2, []client.Mapping{
|
||||
{Pattern: "mc.local", Destination: dest.addr},
|
||||
})
|
||||
|
||||
pc := dialPlayer(t, hubAddr, "mc.local")
|
||||
playerEcho(t, pc, []byte("bye soon"))
|
||||
_ = pc.Close()
|
||||
|
||||
select {
|
||||
case <-dest.connClosed:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatalf("destination did not observe the player disconnect")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDestinationDisconnectPropagates: destination closing → client FIN → player EOF.
|
||||
func TestDestinationDisconnectPropagates(t *testing.T) {
|
||||
const psk = "e2e-destclose"
|
||||
port := freePort(t)
|
||||
hubAddr := fmt.Sprintf("127.0.0.1:%d", port)
|
||||
startHub(t, port, psk)
|
||||
dest := newMockDest(t, modeEchoOnceClose)
|
||||
startClient(t, hubAddr, psk, 2, []client.Mapping{
|
||||
{Pattern: "mc.local", Destination: dest.addr},
|
||||
})
|
||||
|
||||
pc := dialPlayer(t, hubAddr, "mc.local")
|
||||
defer pc.Close()
|
||||
payload := []byte("one shot")
|
||||
if _, err := pc.Write(payload); err != nil {
|
||||
t.Fatalf("write: %v", err)
|
||||
}
|
||||
got := make([]byte, len(payload))
|
||||
_ = pc.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
if _, err := io.ReadFull(pc, got); err != nil {
|
||||
t.Fatalf("read echo: %v", err)
|
||||
}
|
||||
// Destination has now closed; the player's next read must reach EOF.
|
||||
_ = pc.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
if _, err := pc.Read(make([]byte, 16)); err == nil {
|
||||
t.Fatalf("expected EOF after destination closed")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBadPSK: a client with the wrong PSK cannot establish a control session.
|
||||
func TestBadPSK(t *testing.T) {
|
||||
const psk = "e2e-correct"
|
||||
port := freePort(t)
|
||||
hubAddr := fmt.Sprintf("127.0.0.1:%d", port)
|
||||
startHub(t, port, psk)
|
||||
dest := newMockDest(t, modeEcho)
|
||||
|
||||
cfg := &client.Config{
|
||||
Server: hubAddr,
|
||||
PSK: "totally-wrong",
|
||||
MaxConn: 2,
|
||||
PingIntervalMs: 20000,
|
||||
Mappings: []client.Mapping{{Pattern: "mc.local", Destination: dest.addr}},
|
||||
}
|
||||
c := client.New(cfg)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer func() { cancel(); c.Close() }()
|
||||
if err := c.Start(ctx); err == nil {
|
||||
t.Fatalf("expected control session to fail with a wrong PSK")
|
||||
}
|
||||
}
|
||||
|
||||
// TestUnmatchedPattern: a player using an unregistered address is dropped.
|
||||
func TestUnmatchedPattern(t *testing.T) {
|
||||
const psk = "e2e-nomatch"
|
||||
port := freePort(t)
|
||||
hubAddr := fmt.Sprintf("127.0.0.1:%d", port)
|
||||
startHub(t, port, psk)
|
||||
dest := newMockDest(t, modeEcho)
|
||||
startClient(t, hubAddr, psk, 2, []client.Mapping{
|
||||
{Pattern: "mc.local", Destination: dest.addr},
|
||||
})
|
||||
|
||||
pc := dialPlayer(t, hubAddr, "unknown.host")
|
||||
defer pc.Close()
|
||||
_ = pc.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
if _, err := pc.Read(make([]byte, 16)); err == nil {
|
||||
t.Fatalf("expected the hub to drop an unmatched player")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
package e2e
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/iceBear67/redapricot/client/wire"
|
||||
)
|
||||
|
||||
// ---- repo / toolchain discovery ----
|
||||
|
||||
func repoRoot() string {
|
||||
_, file, _, _ := runtime.Caller(0)
|
||||
return filepath.Dir(filepath.Dir(file)) // e2e/ -> repo root
|
||||
}
|
||||
|
||||
func resolveJava(t *testing.T) string {
|
||||
t.Helper()
|
||||
if jh := os.Getenv("JAVA_HOME"); jh != "" {
|
||||
p := filepath.Join(jh, "bin", "java")
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return p
|
||||
}
|
||||
}
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
p := filepath.Join(home, ".sdkman/candidates/java/current/bin/java")
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return p
|
||||
}
|
||||
}
|
||||
if p, err := exec.LookPath("java"); err == nil {
|
||||
return p
|
||||
}
|
||||
t.Skip("java not found (set JAVA_HOME)")
|
||||
return ""
|
||||
}
|
||||
|
||||
// ---- hub subprocess ----
|
||||
|
||||
func freePort(t *testing.T) int {
|
||||
t.Helper()
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("free port: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
return ln.Addr().(*net.TCPAddr).Port
|
||||
}
|
||||
|
||||
// startHub launches the Java hub on the given port and blocks until it accepts
|
||||
// connections. The process is killed on test cleanup.
|
||||
func startHub(t *testing.T, port int, psk string) {
|
||||
t.Helper()
|
||||
install := filepath.Join(repoRoot(), "server", "build", "install", "redapricot-server")
|
||||
if _, err := os.Stat(install); err != nil {
|
||||
t.Fatalf("hub not built at %s (run scripts/build.sh first): %v", install, err)
|
||||
}
|
||||
cfg := fmt.Sprintf(`{"listen":"127.0.0.1:%d","psk":%q,"timestampWindowMs":30000,"pendingTimeoutMs":5000}`, port, psk)
|
||||
cfgPath := filepath.Join(t.TempDir(), "hub.json")
|
||||
if err := os.WriteFile(cfgPath, []byte(cfg), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
java := resolveJava(t)
|
||||
cp := filepath.Join(install, "lib", "*")
|
||||
cmd := exec.Command(java, "-cp", cp, "io.icybear.redapricot.Main", cfgPath)
|
||||
cmd.Stdout = &prefixWriter{prefix: "[hub] "}
|
||||
cmd.Stderr = cmd.Stdout
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("start hub: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = cmd.Process.Kill()
|
||||
_, _ = cmd.Process.Wait()
|
||||
})
|
||||
waitPort(t, fmt.Sprintf("127.0.0.1:%d", port), 30*time.Second)
|
||||
}
|
||||
|
||||
func waitPort(t *testing.T, addr string, timeout time.Duration) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
c, err := net.DialTimeout("tcp", addr, 500*time.Millisecond)
|
||||
if err == nil {
|
||||
_ = c.Close()
|
||||
return
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("hub did not come up on %s within %s", addr, timeout)
|
||||
}
|
||||
|
||||
type prefixWriter struct {
|
||||
prefix string
|
||||
mu sync.Mutex
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func (w *prefixWriter) Write(p []byte) (int, error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
w.buf = append(w.buf, p...)
|
||||
for {
|
||||
i := bytes.IndexByte(w.buf, '\n')
|
||||
if i < 0 {
|
||||
break
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "%s%s\n", w.prefix, w.buf[:i])
|
||||
w.buf = w.buf[i+1:]
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// ---- mock Minecraft destination ----
|
||||
|
||||
type destMode int
|
||||
|
||||
const (
|
||||
modeEcho destMode = iota // echo every post-handshake byte
|
||||
modeEchoOnceClose // echo one read, then close the connection
|
||||
)
|
||||
|
||||
type proxyInfo struct {
|
||||
srcIP net.IP
|
||||
srcPort int
|
||||
dstIP net.IP
|
||||
dstPort int
|
||||
}
|
||||
|
||||
type destEvent struct {
|
||||
handshakeAddr string
|
||||
hasProxy bool
|
||||
proxy proxyInfo
|
||||
}
|
||||
|
||||
var proxyV2Signature = []byte{
|
||||
0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A,
|
||||
}
|
||||
|
||||
type mockDest struct {
|
||||
ln net.Listener
|
||||
addr string
|
||||
mode destMode
|
||||
events chan destEvent
|
||||
connClosed chan struct{}
|
||||
}
|
||||
|
||||
func newMockDest(t *testing.T, mode destMode) *mockDest {
|
||||
t.Helper()
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("mock dest listen: %v", err)
|
||||
}
|
||||
d := &mockDest{
|
||||
ln: ln,
|
||||
addr: ln.Addr().String(),
|
||||
mode: mode,
|
||||
events: make(chan destEvent, 128),
|
||||
connClosed: make(chan struct{}, 128),
|
||||
}
|
||||
t.Cleanup(func() { _ = ln.Close() })
|
||||
go d.serve()
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *mockDest) serve() {
|
||||
for {
|
||||
conn, err := d.ln.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go d.handle(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *mockDest) handle(conn net.Conn) {
|
||||
defer func() {
|
||||
_ = conn.Close()
|
||||
d.connClosed <- struct{}{}
|
||||
}()
|
||||
br := bufio.NewReader(conn)
|
||||
|
||||
var ev destEvent
|
||||
if sig, err := br.Peek(12); err == nil && bytes.Equal(sig, proxyV2Signature) {
|
||||
hdr := make([]byte, 16)
|
||||
if _, err := io.ReadFull(br, hdr); err != nil {
|
||||
return
|
||||
}
|
||||
famProto := hdr[13]
|
||||
addrLen := int(binary.BigEndian.Uint16(hdr[14:16]))
|
||||
block := make([]byte, addrLen)
|
||||
if _, err := io.ReadFull(br, block); err != nil {
|
||||
return
|
||||
}
|
||||
ev.hasProxy = true
|
||||
ev.proxy = parseProxyAddr(famProto, block)
|
||||
}
|
||||
|
||||
// Minecraft handshake packet.
|
||||
pktLen, err := wire.ReadVarInt(br)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pkt := make([]byte, pktLen)
|
||||
if _, err := io.ReadFull(br, pkt); err != nil {
|
||||
return
|
||||
}
|
||||
rr := wire.NewReader(pkt)
|
||||
_, _ = rr.VarInt() // packet id
|
||||
_, _ = rr.VarInt() // protocol version
|
||||
addr, _ := rr.String()
|
||||
ev.handshakeAddr = addr
|
||||
d.events <- ev
|
||||
|
||||
switch d.mode {
|
||||
case modeEcho:
|
||||
_, _ = io.Copy(conn, br) // echo until the peer closes
|
||||
case modeEchoOnceClose:
|
||||
buf := make([]byte, 4096)
|
||||
n, _ := br.Read(buf)
|
||||
if n > 0 {
|
||||
_, _ = conn.Write(buf[:n])
|
||||
}
|
||||
// fallthrough to close via defer
|
||||
}
|
||||
}
|
||||
|
||||
func (d *mockDest) waitEvent(t *testing.T, timeout time.Duration) destEvent {
|
||||
t.Helper()
|
||||
select {
|
||||
case ev := <-d.events:
|
||||
return ev
|
||||
case <-time.After(timeout):
|
||||
t.Fatalf("destination received no connection within %s", timeout)
|
||||
return destEvent{}
|
||||
}
|
||||
}
|
||||
|
||||
func parseProxyAddr(famProto byte, block []byte) proxyInfo {
|
||||
var pi proxyInfo
|
||||
switch famProto {
|
||||
case 0x11: // TCP/IPv4
|
||||
if len(block) >= 12 {
|
||||
pi.srcIP = net.IP(block[0:4])
|
||||
pi.dstIP = net.IP(block[4:8])
|
||||
pi.srcPort = int(binary.BigEndian.Uint16(block[8:10]))
|
||||
pi.dstPort = int(binary.BigEndian.Uint16(block[10:12]))
|
||||
}
|
||||
case 0x21: // TCP/IPv6
|
||||
if len(block) >= 36 {
|
||||
pi.srcIP = net.IP(block[0:16])
|
||||
pi.dstIP = net.IP(block[16:32])
|
||||
pi.srcPort = int(binary.BigEndian.Uint16(block[32:34]))
|
||||
pi.dstPort = int(binary.BigEndian.Uint16(block[34:36]))
|
||||
}
|
||||
}
|
||||
return pi
|
||||
}
|
||||
|
||||
// ---- player simulator ----
|
||||
|
||||
// dialPlayer connects to the hub and sends a Minecraft Handshake (login intent)
|
||||
// with the given server address, returning the open connection.
|
||||
func dialPlayer(t *testing.T, hubAddr, address string) net.Conn {
|
||||
t.Helper()
|
||||
conn, err := net.DialTimeout("tcp", hubAddr, 5*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("player dial: %v", err)
|
||||
}
|
||||
hs := wire.BuildHandshake(767, address, 25565, 2) // intent 2 = login
|
||||
if _, err := conn.Write(hs); err != nil {
|
||||
t.Fatalf("player handshake: %v", err)
|
||||
}
|
||||
return conn
|
||||
}
|
||||
|
||||
// playerEcho sends payload and asserts the same bytes come back (proving the
|
||||
// full player↔destination round-trip works).
|
||||
func playerEcho(t *testing.T, conn net.Conn, payload []byte) {
|
||||
t.Helper()
|
||||
if _, err := conn.Write(payload); err != nil {
|
||||
t.Fatalf("player write: %v", err)
|
||||
}
|
||||
got := make([]byte, len(payload))
|
||||
_ = conn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||
if _, err := io.ReadFull(conn, got); err != nil {
|
||||
t.Fatalf("player read echo: %v", err)
|
||||
}
|
||||
_ = conn.SetReadDeadline(time.Time{})
|
||||
if !bytes.Equal(got, payload) {
|
||||
t.Fatalf("echo mismatch: sent %q got %q", payload, got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user