Join one FM-Funknetz talk group from a headset window.
The station reads ~/.funknetz.json, speaks protocol 2.0, and builds an unsigned funknetz.app. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user