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)) } }