The station reads ~/.funknetz.json, speaks protocol 2.0, and builds an unsigned funknetz.app. Co-authored-by: Cursor <cursoragent@cursor.com>
326 lines
6.7 KiB
Go
326 lines
6.7 KiB
Go
package station_test
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.neitzel.de/konrad/funknetz/station"
|
|
)
|
|
|
|
type fakeClock struct {
|
|
mu sync.Mutex
|
|
now time.Time
|
|
waiters []*fakeWaiter
|
|
gen int
|
|
}
|
|
|
|
type fakeWaiter struct {
|
|
deadline time.Time
|
|
ch chan time.Time
|
|
done bool
|
|
}
|
|
|
|
func newFakeClock() *fakeClock {
|
|
return &fakeClock{now: time.Date(2026, 10, 3, 12, 0, 0, 0, time.UTC)}
|
|
}
|
|
|
|
func (c *fakeClock) Now() time.Time {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return c.now
|
|
}
|
|
|
|
func (c *fakeClock) NewTimer(d time.Duration) station.Timer {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if d < 0 {
|
|
d = 0
|
|
}
|
|
ch := make(chan time.Time, 1)
|
|
c.gen++
|
|
w := &fakeWaiter{deadline: c.now.Add(d), ch: ch}
|
|
if d == 0 {
|
|
ch <- c.now
|
|
w.done = true
|
|
return &fakeTimer{c: c, w: w}
|
|
}
|
|
c.waiters = append(c.waiters, w)
|
|
return &fakeTimer{c: c, w: w}
|
|
}
|
|
|
|
func (c *fakeClock) Advance(d time.Duration) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.now = c.now.Add(d)
|
|
kept := c.waiters[:0]
|
|
for _, w := range c.waiters {
|
|
if w.done {
|
|
continue
|
|
}
|
|
if !w.deadline.After(c.now) {
|
|
w.done = true
|
|
w.ch <- c.now
|
|
continue
|
|
}
|
|
kept = append(kept, w)
|
|
}
|
|
c.waiters = kept
|
|
}
|
|
|
|
func (c *fakeClock) Generation() int {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return c.gen
|
|
}
|
|
|
|
func waitClock(t *testing.T, c *fakeClock, after int) {
|
|
t.Helper()
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for c.Generation() <= after {
|
|
if time.Now().After(deadline) {
|
|
t.Fatal("station did not rearm the clock")
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
}
|
|
|
|
func (c *fakeClock) BlockUntilWaiting(t *testing.T) {
|
|
t.Helper()
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for {
|
|
c.mu.Lock()
|
|
n := 0
|
|
for _, w := range c.waiters {
|
|
if !w.done {
|
|
n++
|
|
}
|
|
}
|
|
c.mu.Unlock()
|
|
if n > 0 {
|
|
return
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatal("station did not wait on the clock")
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
}
|
|
|
|
type fakeTimer struct {
|
|
c *fakeClock
|
|
w *fakeWaiter
|
|
}
|
|
|
|
func (t *fakeTimer) C() <-chan time.Time { return t.w.ch }
|
|
|
|
func (t *fakeTimer) Stop() bool {
|
|
t.c.mu.Lock()
|
|
defer t.c.mu.Unlock()
|
|
if t.w.done {
|
|
return false
|
|
}
|
|
t.w.done = true
|
|
return true
|
|
}
|
|
|
|
type reflector struct {
|
|
ln net.Listener
|
|
udp *net.UDPConn
|
|
tcp net.Conn
|
|
peer *net.UDPAddr
|
|
port int
|
|
}
|
|
|
|
func newReflector(t *testing.T) *reflector {
|
|
t.Helper()
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
port := ln.Addr().(*net.TCPAddr).Port
|
|
udp, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: port})
|
|
if err != nil {
|
|
ln.Close()
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if ln != nil {
|
|
ln.Close()
|
|
}
|
|
udp.Close()
|
|
})
|
|
return &reflector{ln: ln, udp: udp, port: port}
|
|
}
|
|
|
|
func (r *reflector) accept(t *testing.T) {
|
|
t.Helper()
|
|
r.ln.(*net.TCPListener).SetDeadline(time.Now().Add(2 * time.Second))
|
|
conn, err := r.ln.Accept()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r.tcp = conn
|
|
t.Cleanup(func() { conn.Close() })
|
|
}
|
|
|
|
func (r *reflector) readFrame(t *testing.T) (uint16, []byte) {
|
|
t.Helper()
|
|
if err := r.tcp.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var hdr [4]byte
|
|
if _, err := io.ReadFull(r.tcp, hdr[:]); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
n := binary.BigEndian.Uint32(hdr[:])
|
|
if n > 32768 {
|
|
t.Fatalf("frame length %d", n)
|
|
}
|
|
buf := make([]byte, n)
|
|
if _, err := io.ReadFull(r.tcp, buf); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(buf) < 2 {
|
|
t.Fatalf("short frame %d", len(buf))
|
|
}
|
|
return binary.BigEndian.Uint16(buf[:2]), buf[2:]
|
|
}
|
|
|
|
func (r *reflector) writeFrame(t *testing.T, typ uint16, body []byte) {
|
|
t.Helper()
|
|
payload := make([]byte, 2+len(body))
|
|
binary.BigEndian.PutUint16(payload, typ)
|
|
copy(payload[2:], body)
|
|
var hdr [4]byte
|
|
binary.BigEndian.PutUint32(hdr[:], uint32(len(payload)))
|
|
if err := r.tcp.SetWriteDeadline(time.Now().Add(2 * time.Second)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := r.tcp.Write(append(hdr[:], payload...)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func (r *reflector) readUDP(t *testing.T) (typ, id, seq uint16, body []byte) {
|
|
t.Helper()
|
|
if err := r.udp.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
buf := make([]byte, 2048)
|
|
n, addr, err := r.udp.ReadFromUDP(buf)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r.peer = addr
|
|
if n < 6 {
|
|
t.Fatalf("short datagram %d", n)
|
|
}
|
|
typ = binary.BigEndian.Uint16(buf[0:2])
|
|
id = binary.BigEndian.Uint16(buf[2:4])
|
|
seq = binary.BigEndian.Uint16(buf[4:6])
|
|
body = append([]byte(nil), buf[6:n]...)
|
|
return typ, id, seq, body
|
|
}
|
|
|
|
func (r *reflector) writeUDP(t *testing.T, typ, id, seq uint16, body []byte) {
|
|
t.Helper()
|
|
if r.peer == nil {
|
|
t.Fatal("no udp peer")
|
|
}
|
|
buf := make([]byte, 6+len(body))
|
|
binary.BigEndian.PutUint16(buf[0:2], typ)
|
|
binary.BigEndian.PutUint16(buf[2:4], id)
|
|
binary.BigEndian.PutUint16(buf[4:6], seq)
|
|
copy(buf[6:], body)
|
|
if err := r.udp.SetWriteDeadline(time.Now().Add(2 * time.Second)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := r.udp.WriteToUDP(buf, r.peer); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func appendU16(b []byte, v uint16) []byte {
|
|
var buf [2]byte
|
|
binary.BigEndian.PutUint16(buf[:], v)
|
|
return append(b, buf[:]...)
|
|
}
|
|
|
|
func appendU32(b []byte, v uint32) []byte {
|
|
var buf [4]byte
|
|
binary.BigEndian.PutUint32(buf[:], v)
|
|
return append(b, buf[:]...)
|
|
}
|
|
|
|
func appendStr(b []byte, s string) []byte {
|
|
b = appendU16(b, uint16(len(s)))
|
|
return append(b, s...)
|
|
}
|
|
|
|
func appendBytes(b []byte, p []byte) []byte {
|
|
b = appendU16(b, uint16(len(p)))
|
|
return append(b, p...)
|
|
}
|
|
|
|
func takeU16(t *testing.T, b []byte) (uint16, []byte) {
|
|
t.Helper()
|
|
if len(b) < 2 {
|
|
t.Fatalf("need 2 bytes, have %d", len(b))
|
|
}
|
|
return binary.BigEndian.Uint16(b[:2]), b[2:]
|
|
}
|
|
|
|
func takeU32(t *testing.T, b []byte) (uint32, []byte) {
|
|
t.Helper()
|
|
if len(b) < 4 {
|
|
t.Fatalf("need 4 bytes, have %d", len(b))
|
|
}
|
|
return binary.BigEndian.Uint32(b[:4]), b[4:]
|
|
}
|
|
|
|
func takeStr(t *testing.T, b []byte) (string, []byte) {
|
|
t.Helper()
|
|
n, rest := takeU16(t, b)
|
|
if len(rest) < int(n) {
|
|
t.Fatalf("string needs %d bytes, have %d", n, len(rest))
|
|
}
|
|
return string(rest[:n]), rest[n:]
|
|
}
|
|
|
|
func takeBytes(t *testing.T, b []byte) ([]byte, []byte) {
|
|
t.Helper()
|
|
n, rest := takeU16(t, b)
|
|
if len(rest) < int(n) {
|
|
t.Fatalf("bytes need %d, have %d", n, len(rest))
|
|
}
|
|
return append([]byte(nil), rest[:n]...), rest[n:]
|
|
}
|
|
|
|
func serverInfo(clientID uint16, nodes, codecs []string) []byte {
|
|
b := appendU16(nil, 0)
|
|
b = appendU16(b, clientID)
|
|
b = appendU16(b, uint16(len(nodes)))
|
|
for _, n := range nodes {
|
|
b = appendStr(b, n)
|
|
}
|
|
b = appendU16(b, uint16(len(codecs)))
|
|
for _, c := range codecs {
|
|
b = appendStr(b, c)
|
|
}
|
|
return b
|
|
}
|
|
|
|
func writeConfigFile(t *testing.T, body string) string {
|
|
t.Helper()
|
|
path := t.TempDir() + "/funknetz.json"
|
|
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
}
|