115 lines
3.8 KiB
Go
115 lines
3.8 KiB
Go
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))
|
|
}
|
|
}
|