Files
kyle 8018f67031 Wake: a target may list several broadcast addresses
Config keeps broadcast (one) and adds broadcasts (a list); exactly one must
be present. Wake.Addresses() returns Broadcast then Broadcasts; checkWake
errors on both-set, neither-or-empty, and non-host:port entries. Waker sends
to every address in order, logging/past a failure so one dead address does
not stop the others, and returns false only when none could be sent.

Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
2026-09-25 13:45:39 -07:00

74 lines
2.2 KiB
Go

package wake_test
import (
"net"
"testing"
"time"
"git.wntrmute.dev/kyle/crossbar/internal/wake"
)
// listener returns a UDP socket on 127.0.0.1 and a channel that gets one value per datagram.
func listener(t *testing.T) (string, <-chan []byte) {
t.Helper()
pc, err := net.ListenPacket("udp4", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { pc.Close() })
got := make(chan []byte, 4)
go func() {
buf := make([]byte, 256)
for {
n, _, err := pc.ReadFrom(buf)
if err != nil {
return
}
b := make([]byte, n)
copy(b, buf[:n])
got <- b
}
}()
return pc.LocalAddr().String(), got
}
func expectPacket(t *testing.T, name string, got <-chan []byte) {
t.Helper()
select {
case b := <-got:
if len(b) != 102 {
t.Errorf("%s: got %d bytes, want a 102-byte magic packet", name, len(b))
}
case <-time.After(2 * time.Second):
t.Errorf("%s: no packet within two seconds", name)
}
}
// A target may name several broadcast addresses (a host that roams between two networks): the
// packet goes to every one of them, and one address that cannot be resolved does not stop the
// others.
func TestWakeSendsToEveryBroadcast(t *testing.T) {
a, gotA := listener(t)
b, gotB := listener(t)
h := &fakeHealth{after: 1 << 30} // never healthy
w := wake.New(map[string]wake.Target{"titan": {MAC: "aa:bb:cc:dd:ee:ff", Broadcasts: []string{a, "256.1.1.1:9", b}, Wait: 300 * time.Millisecond}}, h)
w.PollEvery(20 * time.Millisecond)
if w.Wake(t.Context(), "titan") {
t.Errorf("Wake must report false when the host never comes up")
}
expectPacket(t, "first address", gotA)
expectPacket(t, "third address, after an unresolvable second", gotB)
}
// The single-address form keeps working, alone or together with the list.
func TestWakeBroadcastAndBroadcastsCombine(t *testing.T) {
a, gotA := listener(t)
b, gotB := listener(t)
h := &fakeHealth{after: 1 << 30} // never healthy
w := wake.New(map[string]wake.Target{"titan": {MAC: "aa:bb:cc:dd:ee:ff", Broadcast: a, Broadcasts: []string{b}, Wait: 300 * time.Millisecond}}, h)
w.PollEvery(20 * time.Millisecond)
w.Wake(t.Context(), "titan")
expectPacket(t, "Broadcast", gotA)
expectPacket(t, "Broadcasts[0]", gotB)
}