Add the wake package: magic packets and a waiter
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
package wake_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.wntrmute.dev/kyle/crossbar/internal/wake"
|
||||
)
|
||||
|
||||
func listen(t *testing.T) (*net.UDPConn, string) {
|
||||
conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { conn.Close() })
|
||||
return conn, conn.LocalAddr().String()
|
||||
}
|
||||
|
||||
func TestMagicPacket(t *testing.T) {
|
||||
pkt, err := wake.MagicPacket("aa:bb:cc:dd:ee:ff")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(pkt) != 102 || !bytes.Equal(pkt[:6], bytes.Repeat([]byte{0xff}, 6)) {
|
||||
t.Fatalf("packet = % x", pkt)
|
||||
}
|
||||
mac := []byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}
|
||||
for i := 0; i < 16; i++ {
|
||||
if !bytes.Equal(pkt[6+6*i:12+6*i], mac) {
|
||||
t.Fatalf("repetition %d wrong: % x", i, pkt[6+6*i:12+6*i])
|
||||
}
|
||||
}
|
||||
for _, bad := range []string{"", "aa:bb", "zz:bb:cc:dd:ee:ff", "aabbccddeeff00"} {
|
||||
if _, err := wake.MagicPacket(bad); err == nil {
|
||||
t.Errorf("MagicPacket(%q) must fail", bad)
|
||||
}
|
||||
}
|
||||
if p2, _ := wake.MagicPacket("AA-BB-CC-DD-EE-FF"); !bytes.Equal(p2, pkt) {
|
||||
t.Errorf("dash-separated upper-case MAC must give the same packet")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendReachesTheBroadcastAddress(t *testing.T) {
|
||||
conn, addr := listen(t)
|
||||
if err := wake.Send("aa:bb:cc:dd:ee:ff", addr); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
buf := make([]byte, 200)
|
||||
_ = conn.SetReadDeadline(time.Now().Add(time.Second))
|
||||
n, _, err := conn.ReadFromUDP(buf)
|
||||
if err != nil || n != 102 {
|
||||
t.Fatalf("received %d bytes, err %v", n, err)
|
||||
}
|
||||
if err := wake.Send("aa:bb:cc:dd:ee:ff", "256.1.1.1:9"); err == nil {
|
||||
t.Error("an unresolvable broadcast address must be an error")
|
||||
}
|
||||
}
|
||||
|
||||
// fakeHealth flips to healthy after `after` calls to Healthy.
|
||||
type fakeHealth struct{ calls, after int }
|
||||
|
||||
func (f *fakeHealth) Healthy(name string) bool { f.calls++; return f.calls > f.after }
|
||||
|
||||
func TestWakerSendsOncePerWindowAndWaitsForHealth(t *testing.T) {
|
||||
conn, addr := listen(t)
|
||||
h := &fakeHealth{after: 3}
|
||||
w := wake.New(map[string]wake.Target{"titan": {MAC: "aa:bb:cc:dd:ee:ff", Broadcast: addr, Wait: 2 * time.Second}}, h)
|
||||
w.PollEvery(20 * time.Millisecond) // test hook: how often Wake re-checks health
|
||||
start := time.Now()
|
||||
ok := w.Wake(context.Background(), "titan")
|
||||
if !ok {
|
||||
t.Fatal("Wake must return true once the host reports healthy")
|
||||
}
|
||||
if time.Since(start) > time.Second {
|
||||
t.Errorf("Wake waited %v for a host that came up after 3 checks", time.Since(start))
|
||||
}
|
||||
_ = conn.SetReadDeadline(time.Now().Add(200 * time.Millisecond))
|
||||
buf := make([]byte, 200)
|
||||
if n, _, err := conn.ReadFromUDP(buf); err != nil || n != 102 {
|
||||
t.Fatalf("no magic packet received: %d %v", n, err)
|
||||
}
|
||||
// A second Wake inside the same window does not send again (the host is booting).
|
||||
_ = w.Wake(context.Background(), "titan")
|
||||
_ = conn.SetReadDeadline(time.Now().Add(150 * time.Millisecond))
|
||||
if n, _, err := conn.ReadFromUDP(buf); err == nil {
|
||||
t.Errorf("a second packet (%d bytes) was sent inside the wait window", n)
|
||||
}
|
||||
if w.Wake(context.Background(), "nobody") {
|
||||
t.Errorf("unknown host: Wake must return false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWakeGivesUpAfterWait(t *testing.T) {
|
||||
_, addr := listen(t)
|
||||
h := &fakeHealth{after: 1 << 30}
|
||||
w := wake.New(map[string]wake.Target{"titan": {MAC: "aa:bb:cc:dd:ee:ff", Broadcast: addr, Wait: 300 * time.Millisecond}}, h)
|
||||
w.PollEvery(20 * time.Millisecond)
|
||||
start := time.Now()
|
||||
if w.Wake(context.Background(), "titan") {
|
||||
t.Fatal("Wake must return false when the host never comes up")
|
||||
}
|
||||
if d := time.Since(start); d < 250*time.Millisecond || d > 900*time.Millisecond {
|
||||
t.Errorf("Wake returned after %v, want about the 300ms wait", d)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||
defer cancel()
|
||||
start = time.Now()
|
||||
if w.Wake(ctx, "titan") || time.Since(start) > 200*time.Millisecond {
|
||||
t.Errorf("a cancelled context must end the wait early (took %v)", time.Since(start))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user