feat(packages): ludic.net - a party over UDP: peers, reliable ordered delivery, the hello's frame and the host's pids, join codes, room codes through a matchmaker with STUN, punching and a relay, the MTU cap and the wire codec; messages in on an inbox, the session's end and a silent guest as facts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
411206e45a
commit
12172d7b75
20 changed files with 1762 additions and 0 deletions
275
packages/ludic.net/tests/net_test.ludic
Normal file
275
packages/ludic.net/tests/net_test.ludic
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
# net_test.ludic - the codec, the codes, a host taking a stranger's hello, reliable delivery in
|
||||
# order, a guest's hello and its timeout, a silent guest and a slot let go - over a fake socket
|
||||
import "ludic.net"
|
||||
import "ludic.base"
|
||||
program NetTest {
|
||||
numbers float
|
||||
property Pkt { ip: int = 0, port: int = 0, len: int = 0, data: []byte }
|
||||
var sent: []Pkt = null
|
||||
var inbound: []Pkt = null
|
||||
var from_ip: int = 0
|
||||
var from_port: int = 0
|
||||
|
||||
function fake_open(port: int) -> int { return 5 }
|
||||
function fake_close(h: int) -> void { }
|
||||
function fake_port(h: int) -> int { return 40000 }
|
||||
function fake_local() -> int { return Udp.ip("10.0.0.5") }
|
||||
function fake_from_ip(h: int) -> int { return from_ip }
|
||||
function fake_from_port(h: int) -> int { return from_port }
|
||||
function fake_send(h: int, ip: int, port: int, b: []byte, n: int) -> int {
|
||||
let p = new Pkt
|
||||
p.ip = ip
|
||||
p.port = port
|
||||
p.len = n
|
||||
p.data = buffer(n)
|
||||
nb_copy(p.data, 0, b, 0, n)
|
||||
push(sent, p)
|
||||
return n
|
||||
}
|
||||
function fake_recv(h: int, b: []byte, cap: int) -> int {
|
||||
if len(inbound) == 0 { return 0 }
|
||||
let p = inbound[0]
|
||||
let rest = new []Pkt
|
||||
for i in 1 .. len(inbound) { push(rest, inbound[i]) }
|
||||
inbound = rest
|
||||
nb_copy(b, 0, p.data, 0, p.len)
|
||||
from_ip = p.ip
|
||||
from_port = p.port
|
||||
return p.len
|
||||
}
|
||||
bind NetSocket { open: fn fake_open, close: fn fake_close, send: fn fake_send, recv: fn fake_recv, from_ip: fn fake_from_ip, from_port: fn fake_from_port, port: fn fake_port, local_ip: fn fake_local }
|
||||
|
||||
function fresh() -> void {
|
||||
sent = new []Pkt
|
||||
inbound = new []Pkt
|
||||
net_reset()
|
||||
q_clear(net_inbox())
|
||||
q_clear(net_facts())
|
||||
}
|
||||
|
||||
# a packet from ip:port: token, pid, ack, then one message of `kind` (reliable with `id` > 0)
|
||||
function arrive(ip: int, port: int, token: int, pid: int, ack: int, kind: int, id: int, payload: int) -> void {
|
||||
let b = buffer(40)
|
||||
nb_put8(b, 0, 'M')
|
||||
nb_put8(b, 1, 'L')
|
||||
nb_put8(b, 2, 1)
|
||||
nb_put8(b, 3, 1)
|
||||
nb_put32(b, 4, token)
|
||||
nb_put32(b, 8, pid)
|
||||
nb_put32(b, 12, ack)
|
||||
nb_put32(b, 16, 1)
|
||||
var o = 20
|
||||
nb_put8(b, o, kind)
|
||||
nb_put8(b, o + 1, 0)
|
||||
o += 2
|
||||
if id > 0 {
|
||||
nb_put8(b, o - 1, 1)
|
||||
nb_put32(b, o, id)
|
||||
o += 4
|
||||
}
|
||||
nb_put8(b, o, 4)
|
||||
nb_put8(b, o + 1, 0)
|
||||
nb_put32(b, o + 2, payload)
|
||||
let p = new Pkt
|
||||
p.ip = ip
|
||||
p.port = port
|
||||
p.len = o + 6
|
||||
p.data = b
|
||||
push(inbound, p)
|
||||
}
|
||||
|
||||
function frame(dt: float) -> void {
|
||||
net_begin(dt)
|
||||
net_update(dt)
|
||||
net_end()
|
||||
}
|
||||
|
||||
# the host with a guest let in at 10.0.0.9:5000 as pid 2
|
||||
function host_with_guest() -> int {
|
||||
fresh()
|
||||
expect(net_host(27415, 77))
|
||||
arrive(Udp.ip("10.0.0.9"), 5000, 0, 0, 0, NET_HELLO, 1, 42)
|
||||
net_begin(0.01)
|
||||
let got = q_drain(net_inbox())
|
||||
expect_eq(len(got), 1)
|
||||
return net_accept(got[0].slot)
|
||||
}
|
||||
|
||||
function facts_of(what: int) -> []NetFact {
|
||||
let all = q_drain(net_facts())
|
||||
let out = new []NetFact
|
||||
for i in 0 .. len(all) { if all[i].what == what { push(out, all[i]) } }
|
||||
return out
|
||||
}
|
||||
|
||||
test "the codec carries ints, bools, floats and text, and a short message reads bad" {
|
||||
fresh()
|
||||
nw_begin()
|
||||
nw_i(-7)
|
||||
nw_b(true)
|
||||
nw_f(2.5)
|
||||
nw_s("maroon")
|
||||
net_read(net_written())
|
||||
expect_eq(nr_i(), -7)
|
||||
expect(nr_b())
|
||||
expect_near(nr_f(), 2.5, 0.0001)
|
||||
expect(nr_s() == "maroon")
|
||||
expect(not nr_bad())
|
||||
expect_eq(nr_i(), 0)
|
||||
expect(nr_bad())
|
||||
}
|
||||
|
||||
test "a join code decodes to its address and port; an address and a room code are taken too" {
|
||||
fresh()
|
||||
let ip = Udp.ip("192.168.1.20")
|
||||
let code = net_code_of(ip, 27415)
|
||||
expect_eq(len(code), 11)
|
||||
expect(net_parse_code(code))
|
||||
expect_eq(net_code_ip(), ip)
|
||||
expect_eq(net_code_port(), 27415)
|
||||
expect(net_parse_code(Text.lower(code)))
|
||||
expect(net_parse_code("10.1.2.3:4000"))
|
||||
expect_eq(net_code_port(), 4000)
|
||||
expect(net_parse_code("10.1.2.3"))
|
||||
expect_eq(net_code_port(), 27415)
|
||||
expect(not net_parse_code("HELLO"))
|
||||
expect(net_is_room("abcdef"))
|
||||
expect(not net_is_room("ABCDE1"))
|
||||
}
|
||||
|
||||
test "a host takes a stranger only with a hello, and hands out pids from 2" {
|
||||
let pid = host_with_guest()
|
||||
expect_eq(pid, 2)
|
||||
expect(net_hosting())
|
||||
expect_eq(net_my_pid(), 1)
|
||||
arrive(Udp.ip("10.0.0.10"), 6000, 0, 0, 0, 9, 1, 1)
|
||||
net_begin(0.01)
|
||||
expect_eq(q_len(net_inbox()), 0)
|
||||
arrive(Udp.ip("10.0.0.11"), 6000, 0, 0, 0, NET_HELLO, 1, 1)
|
||||
net_begin(0.01)
|
||||
let got = q_drain(net_inbox())
|
||||
expect_eq(net_accept(got[0].slot), 3)
|
||||
}
|
||||
|
||||
test "a message's payload reads back as it was written" {
|
||||
host_with_guest()
|
||||
arrive(Udp.ip("10.0.0.9"), 5000, 77, 2, 0, 9, 2, 1234)
|
||||
net_begin(0.01)
|
||||
let got = q_drain(net_inbox())
|
||||
expect_eq(len(got), 1)
|
||||
expect_eq(got[0].kind, 9)
|
||||
expect_eq(got[0].from, 2)
|
||||
net_read(got[0])
|
||||
expect_eq(nr_i(), 1234)
|
||||
expect(not nr_bad())
|
||||
nr_i()
|
||||
expect(nr_bad())
|
||||
}
|
||||
|
||||
test "a reliable message that arrives early is dropped and taken in order" {
|
||||
let slot = np_of_pid(host_with_guest())
|
||||
arrive(Udp.ip("10.0.0.9"), 5000, 77, 2, 0, 9, 3, 30)
|
||||
net_begin(0.01)
|
||||
expect_eq(q_len(net_inbox()), 0)
|
||||
arrive(Udp.ip("10.0.0.9"), 5000, 77, 2, 0, 9, 2, 20)
|
||||
arrive(Udp.ip("10.0.0.9"), 5000, 77, 2, 0, 9, 3, 30)
|
||||
net_begin(0.01)
|
||||
let got = q_drain(net_inbox())
|
||||
expect_eq(len(got), 2)
|
||||
net_read(got[1])
|
||||
expect_eq(nr_i(), 30)
|
||||
expect_eq(got[1].slot, slot)
|
||||
}
|
||||
|
||||
test "a reliable message goes again until it is taken" {
|
||||
let slot = np_of_pid(host_with_guest())
|
||||
net_end()
|
||||
sent = new []Pkt
|
||||
nw_begin()
|
||||
nw_i(5)
|
||||
np_queue(slot, 9, true)
|
||||
frame(0.01)
|
||||
expect_eq(len(sent), 1)
|
||||
frame(0.1)
|
||||
expect_eq(len(sent), 1)
|
||||
frame(0.2)
|
||||
expect_eq(len(sent), 2)
|
||||
arrive(Udp.ip("10.0.0.9"), 5000, 77, 2, 1, 9, 0, 0)
|
||||
frame(0.3)
|
||||
let n = len(sent)
|
||||
frame(0.3)
|
||||
expect_eq(len(sent), n)
|
||||
}
|
||||
|
||||
test "an unreliable message half a second old is dropped" {
|
||||
let slot = np_of_pid(host_with_guest())
|
||||
net_end()
|
||||
sent = new []Pkt
|
||||
nw_begin()
|
||||
np_queue(slot, 9, false)
|
||||
net_begin(0.6)
|
||||
net_end()
|
||||
expect_eq(len(sent), 0)
|
||||
}
|
||||
|
||||
test "a guest sends its hello until nobody has answered for too long" {
|
||||
fresh()
|
||||
nw_begin()
|
||||
nw_s("v1")
|
||||
net_hello_set()
|
||||
expect(net_join(Udp.ip("10.0.0.1"), 27415))
|
||||
expect(net_joining())
|
||||
frame(0.1)
|
||||
expect_eq(len(sent), 1)
|
||||
expect_eq(nb_get8(sent[0].data, 20), NET_HELLO)
|
||||
for i in 0 .. 110 { frame(0.1) }
|
||||
expect_eq(net_state(), NET_LOST)
|
||||
let down = facts_of(NET_DOWN)
|
||||
expect_eq(len(down), 1)
|
||||
expect_eq(down[0].why, NET_WHY_NO_ANSWER)
|
||||
net_fresh()
|
||||
expect_eq(net_state(), NET_IDLE)
|
||||
}
|
||||
|
||||
test "a welcome makes the guest that pid in that session" {
|
||||
fresh()
|
||||
nw_begin()
|
||||
net_hello_set()
|
||||
net_join(Udp.ip("10.0.0.1"), 27415)
|
||||
net_joined_as(99, 4)
|
||||
expect(net_live())
|
||||
expect_eq(net_my_pid(), 4)
|
||||
expect_eq(net_token(), 99)
|
||||
}
|
||||
|
||||
test "a guest silent for the timeout is gone" {
|
||||
let pid = host_with_guest()
|
||||
let slot = np_of_pid(pid)
|
||||
for i in 0 .. 105 { frame(0.1) }
|
||||
let gone = facts_of(NET_GONE)
|
||||
expect_eq(len(gone), 1)
|
||||
expect_eq(gone[0].pid, pid)
|
||||
expect_eq(gone[0].slot, slot)
|
||||
expect(not net_peer_on(slot))
|
||||
}
|
||||
|
||||
test "a slot is let go once it has taken its last message" {
|
||||
let slot = np_of_pid(host_with_guest())
|
||||
nw_begin()
|
||||
np_queue(slot, 9, true)
|
||||
net_close_when_sent(slot, 20.0)
|
||||
frame(0.01)
|
||||
expect(net_peer_on(slot))
|
||||
arrive(Udp.ip("10.0.0.9"), 5000, 77, 2, 1, 9, 0, 0)
|
||||
frame(0.01)
|
||||
expect(not net_peer_on(slot))
|
||||
expect_eq(len(facts_of(NET_CLOSED)), 1)
|
||||
}
|
||||
|
||||
test "with no matchmaker, a room code is refused" {
|
||||
fresh()
|
||||
expect(not net_room_on())
|
||||
expect(not net_room_join("ABCDEF"))
|
||||
expect_eq(net_why(), NET_WHY_NO_MATCHMAKER)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue