mirror of
https://github.com/Quad4-Software/Reticulum-Go
synced 2026-08-29 23:48:44 -04:00
613 lines
13 KiB
Go
613 lines
13 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright (c) 2024-2026 Quad4.io
|
|
|
|
package backbone
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"runtime"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestInitIdempotent(t *testing.T) {
|
|
Shutdown()
|
|
h1, err := Init(BackendGo)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h2, err := Init(BackendEpoll)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if h1 != h2 {
|
|
t.Fatal("expected same global hub instance")
|
|
}
|
|
t.Cleanup(Shutdown)
|
|
}
|
|
|
|
func TestDefaultBackendPlatform(t *testing.T) {
|
|
b := DefaultBackend()
|
|
switch runtime.GOOS {
|
|
case "linux", "android":
|
|
if b != BackendEpoll {
|
|
t.Fatalf("linux default=%s", b)
|
|
}
|
|
case "darwin", "freebsd", "netbsd", "openbsd":
|
|
if b != BackendKqueue {
|
|
t.Fatalf("bsd default=%s", b)
|
|
}
|
|
default:
|
|
if b != BackendGo {
|
|
t.Fatalf("generic default=%s", b)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHubEchoAllBackends(t *testing.T) {
|
|
for _, backend := range testableBackends(t) {
|
|
t.Run(string(backend), func(t *testing.T) {
|
|
testHubEcho(t, backend)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testHubEcho(t *testing.T, backend Backend) {
|
|
t.Helper()
|
|
hub := testHubWithBackend(t, backend)
|
|
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = ln.Close() })
|
|
|
|
var (
|
|
serverStream *Stream
|
|
serverReady = make(chan struct{})
|
|
serverOnce sync.Once
|
|
)
|
|
if err := hub.RegisterListener(ln, func(conn net.Conn) {
|
|
var err error
|
|
serverStream, err = hub.RegisterStream(conn, 1<<20, func(frame []byte) {
|
|
serverStream.QueueSend(frame)
|
|
}, nil)
|
|
if err != nil {
|
|
t.Errorf("server stream: %v", err)
|
|
return
|
|
}
|
|
serverOnce.Do(func() { close(serverReady) })
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
clientConn, err := net.Dial("tcp", ln.Addr().String())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = clientConn.Close() })
|
|
|
|
payload := bytes.Repeat([]byte{0xAB}, 512)
|
|
got := make(chan []byte, 1)
|
|
clientStream, err := hub.RegisterStream(clientConn, 1<<20, func(frame []byte) {
|
|
select {
|
|
case got <- append([]byte(nil), frame...):
|
|
default:
|
|
}
|
|
}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
select {
|
|
case <-serverReady:
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("accept timeout")
|
|
}
|
|
|
|
clientStream.QueueSend(payload)
|
|
select {
|
|
case rcv := <-got:
|
|
if !bytes.Equal(rcv, payload) {
|
|
t.Fatalf("payload mismatch len=%d", len(rcv))
|
|
}
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("echo timeout")
|
|
}
|
|
}
|
|
|
|
func TestHubManyConcurrentClients(t *testing.T) {
|
|
for _, backend := range testableBackends(t) {
|
|
t.Run(string(backend), func(t *testing.T) {
|
|
testHubManyClients(t, backend, 32)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testHubManyClients(t *testing.T, backend Backend, n int) {
|
|
t.Helper()
|
|
hub := testHubWithBackend(t, backend)
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = ln.Close() })
|
|
|
|
var served atomic.Int32
|
|
if err := hub.RegisterListener(ln, func(conn net.Conn) {
|
|
served.Add(1)
|
|
_, _ = hub.RegisterStream(conn, 4096, nil, func() { _ = conn.Close() })
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(n)
|
|
for i := range n {
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
c, err := net.Dial("tcp", ln.Addr().String())
|
|
if err != nil {
|
|
t.Errorf("dial %d: %v", id, err)
|
|
return
|
|
}
|
|
defer c.Close()
|
|
time.Sleep(10 * time.Millisecond)
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
|
|
deadline := time.Now().Add(3 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if int(served.Load()) >= n {
|
|
return
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
t.Fatalf("served %d want %d", served.Load(), n)
|
|
}
|
|
|
|
func TestHubLargePayload(t *testing.T) {
|
|
hub := testHubWithBackend(t, BackendGo)
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = ln.Close() })
|
|
|
|
ready := make(chan *Stream, 1)
|
|
if err := hub.RegisterListener(ln, func(conn net.Conn) {
|
|
var st *Stream
|
|
var regErr error
|
|
st, regErr = hub.RegisterStream(conn, 1<<20, func(frame []byte) {
|
|
st.QueueSend(frame)
|
|
}, nil)
|
|
if regErr != nil {
|
|
t.Errorf("stream: %v", regErr)
|
|
return
|
|
}
|
|
ready <- st
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
c, err := net.Dial("tcp", ln.Addr().String())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = c.Close() })
|
|
|
|
payload := bytes.Repeat([]byte{0xCD}, 64*1024)
|
|
done := make(chan []byte, 1)
|
|
cs, err := hub.RegisterStream(c, 1<<20, func(frame []byte) {
|
|
done <- append([]byte(nil), frame...)
|
|
}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
select {
|
|
case <-ready:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("server timeout")
|
|
}
|
|
|
|
cs.QueueSend(payload)
|
|
select {
|
|
case got := <-done:
|
|
if !bytes.Equal(got, payload) {
|
|
t.Fatalf("large payload mismatch: got %d want %d", len(got), len(payload))
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("large echo timeout")
|
|
}
|
|
}
|
|
|
|
func TestHubCloseDrains(t *testing.T) {
|
|
hub := testHubWithBackend(t, BackendGo)
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := hub.RegisterListener(ln, func(net.Conn) {}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
hub.Close()
|
|
}
|
|
|
|
func TestStreamDoubleClose(t *testing.T) {
|
|
hub := testHubWithBackend(t, BackendGo)
|
|
c1, c2 := net.Pipe()
|
|
t.Cleanup(func() { _ = c1.Close(); _ = c2.Close() })
|
|
|
|
s, err := hub.RegisterStream(c1, 1024, nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s.Close()
|
|
s.Close()
|
|
}
|
|
|
|
func TestParseBackendAliases(t *testing.T) {
|
|
if ParseBackend("kqueue") != BackendKqueue {
|
|
t.Fatal("kqueue")
|
|
}
|
|
if ParseBackend("go") != BackendGo {
|
|
t.Fatal("go")
|
|
}
|
|
if ParseBackend("unknown-value") != BackendAuto {
|
|
t.Fatal("unknown")
|
|
}
|
|
}
|
|
|
|
func TestInitUringFallback(t *testing.T) {
|
|
if runtime.GOOS != "linux" {
|
|
t.Skip("linux only")
|
|
}
|
|
if !UringProbeAllowed() {
|
|
t.Skip("io_uring probe disabled in CI")
|
|
}
|
|
Shutdown()
|
|
hub, err := Init(BackendUring)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(Shutdown)
|
|
if hub.Backend() != BackendUring && hub.Backend() != BackendEpoll {
|
|
t.Fatalf("unexpected backend %s", hub.Backend())
|
|
}
|
|
}
|
|
|
|
// Race: concurrent QueueSend on one stream over TCP.
|
|
func TestRaceStreamConcurrentSend(t *testing.T) {
|
|
hub := testHubWithBackend(t, BackendGo)
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = ln.Close() })
|
|
|
|
ready := make(chan *Stream, 1)
|
|
if err := hub.RegisterListener(ln, func(conn net.Conn) {
|
|
var st *Stream
|
|
var regErr error
|
|
st, regErr = hub.RegisterStream(conn, 1<<20, func([]byte) {}, nil)
|
|
if regErr != nil {
|
|
t.Errorf("stream: %v", regErr)
|
|
return
|
|
}
|
|
ready <- st
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
c, err := net.Dial("tcp", ln.Addr().String())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = c.Close() })
|
|
|
|
var st *Stream
|
|
select {
|
|
case st = <-ready:
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("accept timeout")
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
for i := range 16 {
|
|
wg.Add(1)
|
|
go func(n int) {
|
|
defer wg.Done()
|
|
for j := range 32 {
|
|
st.QueueSend([]byte{byte(n), byte(j), 0x01})
|
|
}
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
// Race: hub shutdown while clients active.
|
|
func TestRaceHubCloseWithActiveStreams(t *testing.T) {
|
|
for range 4 {
|
|
hub := testHubWithBackend(t, BackendGo)
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = hub.RegisterListener(ln, func(conn net.Conn) {
|
|
_, _ = hub.RegisterStream(conn, 4096, func(frame []byte) {
|
|
// echo
|
|
}, nil)
|
|
})
|
|
var wg sync.WaitGroup
|
|
for range 8 {
|
|
wg.Go(func() {
|
|
c, err := net.Dial("tcp", ln.Addr().String())
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer c.Close()
|
|
s, err := hub.RegisterStream(c, 4096, nil, nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
for range 16 {
|
|
s.QueueSend([]byte{1, 2, 3})
|
|
}
|
|
})
|
|
}
|
|
wg.Wait()
|
|
_ = ln.Close()
|
|
hub.Close()
|
|
Shutdown()
|
|
}
|
|
}
|
|
|
|
// Race: accept storm.
|
|
func TestRaceAcceptStorm(t *testing.T) {
|
|
hub := testHubWithBackend(t, DefaultBackend())
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = ln.Close() })
|
|
|
|
var accepted atomic.Int32
|
|
if err := hub.RegisterListener(ln, func(conn net.Conn) {
|
|
accepted.Add(1)
|
|
_, _ = hub.RegisterStream(conn, 2048, nil, nil)
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
for range 64 {
|
|
wg.Go(func() {
|
|
c, err := net.Dial("tcp", ln.Addr().String())
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer c.Close()
|
|
})
|
|
}
|
|
wg.Wait()
|
|
time.Sleep(200 * time.Millisecond)
|
|
if accepted.Load() == 0 {
|
|
t.Fatal("no connections accepted")
|
|
}
|
|
}
|
|
|
|
func TestHubWireFormatMatchesInterfacesPackage(t *testing.T) {
|
|
payload := bytes.Repeat([]byte{0x01, hdlcFlag, hdlcEsc, 0xFF}, 6)
|
|
backboneFrame := frameHDLC(payload)
|
|
if backboneFrame[0] != hdlcFlag || backboneFrame[len(backboneFrame)-1] != hdlcFlag {
|
|
t.Fatal("missing frame flags")
|
|
}
|
|
var got []byte
|
|
d := NewHDLCDecoder(4096, func(pkt []byte) { got = pkt })
|
|
d.Feed(backboneFrame)
|
|
if !bytes.Equal(got, payload) {
|
|
t.Fatalf("decode got %x want %x", got, payload)
|
|
}
|
|
}
|
|
|
|
func TestHubBurstManyFramesOneWrite(t *testing.T) {
|
|
hub := testHubWithBackend(t, BackendGo)
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = ln.Close() })
|
|
|
|
const n = 128
|
|
got := make(chan []byte, n)
|
|
if err := hub.RegisterListener(ln, func(conn net.Conn) {
|
|
_, err := hub.RegisterStream(conn, 500, func(frame []byte) {
|
|
got <- append([]byte(nil), frame...)
|
|
}, nil)
|
|
if err != nil {
|
|
t.Errorf("register: %v", err)
|
|
}
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
c, err := net.Dial("tcp", ln.Addr().String())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = c.Close() })
|
|
|
|
var blob []byte
|
|
want := make([][]byte, n)
|
|
for i := range n {
|
|
p := bytes.Repeat([]byte{byte(i + 20)}, 24)
|
|
p[2], p[3] = hdlcFlag, hdlcEsc
|
|
want[i] = p
|
|
blob = append(blob, frameHDLC(p)...)
|
|
}
|
|
if _, err := c.Write(blob); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
deadline := time.After(3 * time.Second)
|
|
var frames [][]byte
|
|
for len(frames) < n {
|
|
select {
|
|
case f := <-got:
|
|
frames = append(frames, f)
|
|
case <-deadline:
|
|
t.Fatalf("got %d frames want %d", len(frames), n)
|
|
}
|
|
}
|
|
for i := range n {
|
|
if !bytes.Equal(frames[i], want[i]) {
|
|
t.Fatalf("frame %d mismatch", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRequeueUnwrittenCompactsRemainder(t *testing.T) {
|
|
s := &Stream{}
|
|
s.requeueUnwritten([]byte{1, 2, 3, 4, 5}, 2, nil)
|
|
if !bytes.Equal(s.txBuf, []byte{3, 4, 5}) {
|
|
t.Fatalf("partial write remainder=%v", s.txBuf)
|
|
}
|
|
s.requeueUnwritten([]byte{9, 8}, 2, nil)
|
|
if !bytes.Equal(s.txBuf, []byte{3, 4, 5}) {
|
|
t.Fatalf("full write must keep queued %v", s.txBuf)
|
|
}
|
|
s.txBuf = nil
|
|
s.requeueUnwritten([]byte{1, 2, 3}, 3, nil)
|
|
if s.txBuf == nil || len(s.txBuf) != 0 {
|
|
t.Fatalf("full write with empty queue should reuse cap, len=%d", len(s.txBuf))
|
|
}
|
|
s.requeueUnwritten([]byte{7, 8, 9}, 0, io.ErrShortWrite)
|
|
if !bytes.Equal(s.txBuf, []byte{7, 8, 9}) {
|
|
t.Fatalf("zero-write error should requeue all %v", s.txBuf)
|
|
}
|
|
}
|
|
|
|
func BenchmarkHubEcho(b *testing.B) {
|
|
for _, backend := range []Backend{BackendGo, DefaultBackend()} {
|
|
b.Run(string(backend), func(b *testing.B) {
|
|
runHubEchoBenchmark(b, backend)
|
|
})
|
|
}
|
|
}
|
|
|
|
func runHubEchoBenchmark(b *testing.B, backend Backend) {
|
|
Shutdown()
|
|
hub, err := Init(backend)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
defer Shutdown()
|
|
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
defer ln.Close()
|
|
|
|
ready := make(chan struct{})
|
|
if err := hub.RegisterListener(ln, func(conn net.Conn) {
|
|
var st *Stream
|
|
var regErr error
|
|
st, regErr = hub.RegisterStream(conn, 1<<20, func(frame []byte) {
|
|
st.QueueSend(frame)
|
|
}, nil)
|
|
if regErr != nil {
|
|
b.Error(regErr)
|
|
return
|
|
}
|
|
close(ready)
|
|
}); err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
|
|
c, err := net.Dial("tcp", ln.Addr().String())
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
defer c.Close()
|
|
|
|
payload := bytes.Repeat([]byte{0x42}, 1024)
|
|
ack := make(chan struct{}, 1)
|
|
cs, err := hub.RegisterStream(c, 1<<20, func([]byte) {
|
|
select {
|
|
case ack <- struct{}{}:
|
|
default:
|
|
}
|
|
}, nil)
|
|
if err != nil {
|
|
b.Fatal(err)
|
|
}
|
|
<-ready
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
cs.QueueSend(payload)
|
|
<-ack
|
|
}
|
|
}
|
|
|
|
func BenchmarkHDLCEscape(b *testing.B) {
|
|
data := bytes.Repeat([]byte{0x7E}, 1024)
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = escapeHDLC(data)
|
|
}
|
|
}
|
|
|
|
func BenchmarkHDLCDecoderFeed(b *testing.B) {
|
|
payload := bytes.Repeat([]byte{0x42}, 512)
|
|
frame := frameHDLC(payload)
|
|
d := NewHDLCDecoder(4096, func([]byte) {})
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
d.Reset()
|
|
d.Feed(frame)
|
|
}
|
|
}
|
|
|
|
func BenchmarkFrameHDLC(b *testing.B) {
|
|
data := make([]byte, 1024)
|
|
for i := range data {
|
|
data[i] = byte(i)
|
|
}
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
_ = frameHDLC(data)
|
|
}
|
|
}
|
|
|
|
func BenchmarkAppendFrameHDLC(b *testing.B) {
|
|
data := make([]byte, 1024)
|
|
for i := range data {
|
|
data[i] = byte(i)
|
|
}
|
|
dst := make([]byte, 0, 2048)
|
|
b.ReportAllocs()
|
|
for i := 0; i < b.N; i++ {
|
|
dst = appendFrameHDLC(dst[:0], data)
|
|
}
|
|
}
|
|
|
|
func ExampleHub() {
|
|
hub, err := Init(BackendGo)
|
|
if err != nil {
|
|
fmt.Println("init failed")
|
|
return
|
|
}
|
|
defer Shutdown()
|
|
fmt.Println(hub.Backend() == BackendGo)
|
|
// Output: true
|
|
}
|