|
|
@@ -0,0 +1,220 @@
|
|
|
+// Copyright (c) Tailscale Inc & AUTHORS
|
|
|
+// SPDX-License-Identifier: BSD-3-Clause
|
|
|
+
|
|
|
+package ipnlocal
|
|
|
+
|
|
|
+import (
|
|
|
+ "context"
|
|
|
+ "reflect"
|
|
|
+ "slices"
|
|
|
+ "testing"
|
|
|
+ "time"
|
|
|
+
|
|
|
+ "tailscale.com/drive"
|
|
|
+ "tailscale.com/ipn"
|
|
|
+ "tailscale.com/tstest"
|
|
|
+ "tailscale.com/tstime"
|
|
|
+ "tailscale.com/types/logger"
|
|
|
+ "tailscale.com/types/netmap"
|
|
|
+ "tailscale.com/types/views"
|
|
|
+)
|
|
|
+
|
|
|
+func TestIsNotableNotify(t *testing.T) {
|
|
|
+ tests := []struct {
|
|
|
+ name string
|
|
|
+ notify *ipn.Notify
|
|
|
+ want bool
|
|
|
+ }{
|
|
|
+ {"nil", nil, false},
|
|
|
+ {"empty", &ipn.Notify{}, false},
|
|
|
+ {"version", &ipn.Notify{Version: "foo"}, false},
|
|
|
+ {"netmap", &ipn.Notify{NetMap: new(netmap.NetworkMap)}, false},
|
|
|
+ {"engine", &ipn.Notify{Engine: new(ipn.EngineStatus)}, false},
|
|
|
+ }
|
|
|
+
|
|
|
+ // Then for all other fields, assume they're notable.
|
|
|
+ // We use reflect to catch fields that might be added in the future without
|
|
|
+ // remembering to update the [isNotableNotify] function.
|
|
|
+ rt := reflect.TypeFor[ipn.Notify]()
|
|
|
+ for i := range rt.NumField() {
|
|
|
+ n := &ipn.Notify{}
|
|
|
+ sf := rt.Field(i)
|
|
|
+ switch sf.Name {
|
|
|
+ case "_", "NetMap", "Engine", "Version":
|
|
|
+ // Already covered above or not applicable.
|
|
|
+ continue
|
|
|
+ case "DriveShares":
|
|
|
+ n.DriveShares = views.SliceOfViews[*drive.Share, drive.ShareView](make([]*drive.Share, 1))
|
|
|
+ default:
|
|
|
+ rf := reflect.ValueOf(n).Elem().Field(i)
|
|
|
+ switch rf.Kind() {
|
|
|
+ case reflect.Pointer:
|
|
|
+ rf.Set(reflect.New(rf.Type().Elem()))
|
|
|
+ case reflect.String:
|
|
|
+ rf.SetString("foo")
|
|
|
+ case reflect.Slice:
|
|
|
+ rf.Set(reflect.MakeSlice(rf.Type(), 1, 1))
|
|
|
+ default:
|
|
|
+ t.Errorf("unhandled field kind %v for %q", rf.Kind(), sf.Name)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ tests = append(tests, struct {
|
|
|
+ name string
|
|
|
+ notify *ipn.Notify
|
|
|
+ want bool
|
|
|
+ }{
|
|
|
+ name: "field-" + rt.Field(i).Name,
|
|
|
+ notify: n,
|
|
|
+ want: true,
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, tt := range tests {
|
|
|
+ if got := isNotableNotify(tt.notify); got != tt.want {
|
|
|
+ t.Errorf("%v: got %v; want %v", tt.name, got, tt.want)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+type rateLimitingBusSenderTester struct {
|
|
|
+ tb testing.TB
|
|
|
+ got []*ipn.Notify
|
|
|
+ clock *tstest.Clock
|
|
|
+ s *rateLimitingBusSender
|
|
|
+}
|
|
|
+
|
|
|
+func (st *rateLimitingBusSenderTester) init() {
|
|
|
+ if st.s != nil {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ st.clock = tstest.NewClock(tstest.ClockOpts{
|
|
|
+ Start: time.Unix(1731777537, 0), // time I wrote this test :)
|
|
|
+ })
|
|
|
+ st.s = &rateLimitingBusSender{
|
|
|
+ clock: tstime.DefaultClock{Clock: st.clock},
|
|
|
+ fn: func(n *ipn.Notify) bool {
|
|
|
+ st.got = append(st.got, n)
|
|
|
+ return true
|
|
|
+ },
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (st *rateLimitingBusSenderTester) send(n *ipn.Notify) {
|
|
|
+ st.tb.Helper()
|
|
|
+ st.init()
|
|
|
+ if !st.s.send(n) {
|
|
|
+ st.tb.Fatal("unexpected send failed")
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (st *rateLimitingBusSenderTester) advance(d time.Duration) {
|
|
|
+ st.tb.Helper()
|
|
|
+ st.clock.Advance(d)
|
|
|
+ select {
|
|
|
+ case <-st.s.flushChan():
|
|
|
+ if !st.s.flush() {
|
|
|
+ st.tb.Fatal("unexpected flush failed")
|
|
|
+ }
|
|
|
+ default:
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestRateLimitingBusSender(t *testing.T) {
|
|
|
+ nm1 := &ipn.Notify{NetMap: new(netmap.NetworkMap)}
|
|
|
+ nm2 := &ipn.Notify{NetMap: new(netmap.NetworkMap)}
|
|
|
+ eng1 := &ipn.Notify{Engine: new(ipn.EngineStatus)}
|
|
|
+ eng2 := &ipn.Notify{Engine: new(ipn.EngineStatus)}
|
|
|
+
|
|
|
+ t.Run("unbuffered", func(t *testing.T) {
|
|
|
+ st := &rateLimitingBusSenderTester{tb: t}
|
|
|
+ st.send(nm1)
|
|
|
+ st.send(nm2)
|
|
|
+ st.send(eng1)
|
|
|
+ st.send(eng2)
|
|
|
+ if !slices.Equal(st.got, []*ipn.Notify{nm1, nm2, eng1, eng2}) {
|
|
|
+ t.Errorf("got %d items; want 4 specific ones, unmodified", len(st.got))
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ t.Run("buffered", func(t *testing.T) {
|
|
|
+ st := &rateLimitingBusSenderTester{tb: t}
|
|
|
+ st.init()
|
|
|
+ st.s.interval = 1 * time.Second
|
|
|
+ st.send(&ipn.Notify{Version: "initial"})
|
|
|
+ if len(st.got) != 1 {
|
|
|
+ t.Fatalf("got %d items; expected 1 (first to flush immediately)", len(st.got))
|
|
|
+ }
|
|
|
+ st.send(nm1)
|
|
|
+ st.send(nm2)
|
|
|
+ st.send(eng1)
|
|
|
+ st.send(eng2)
|
|
|
+ if len(st.got) != 1 {
|
|
|
+ if len(st.got) != 1 {
|
|
|
+ t.Fatalf("got %d items; expected still just that first 1", len(st.got))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // But moving the clock should flush the rest, collasced into one new one.
|
|
|
+ st.advance(5 * time.Second)
|
|
|
+ if len(st.got) != 2 {
|
|
|
+ t.Fatalf("got %d items; want 2", len(st.got))
|
|
|
+ }
|
|
|
+ gotn := st.got[1]
|
|
|
+ if gotn.NetMap != nm2.NetMap {
|
|
|
+ t.Errorf("got wrong NetMap; got %p", gotn.NetMap)
|
|
|
+ }
|
|
|
+ if gotn.Engine != eng2.Engine {
|
|
|
+ t.Errorf("got wrong Engine; got %p", gotn.Engine)
|
|
|
+ }
|
|
|
+ if t.Failed() {
|
|
|
+ t.Logf("failed Notify was: %v", logger.AsJSON(gotn))
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // Test the Run method
|
|
|
+ t.Run("run", func(t *testing.T) {
|
|
|
+ st := &rateLimitingBusSenderTester{tb: t}
|
|
|
+ st.init()
|
|
|
+ st.s.interval = 1 * time.Second
|
|
|
+ st.s.lastFlush = st.clock.Now() // pretend we just flushed
|
|
|
+
|
|
|
+ flushc := make(chan *ipn.Notify, 1)
|
|
|
+ st.s.fn = func(n *ipn.Notify) bool {
|
|
|
+ flushc <- n
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ didSend := make(chan bool, 2)
|
|
|
+ st.s.didSendTestHook = func() { didSend <- true }
|
|
|
+ waitSend := func() {
|
|
|
+ select {
|
|
|
+ case <-didSend:
|
|
|
+ case <-time.After(5 * time.Second):
|
|
|
+ t.Error("timeout waiting for call to send")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ctx, cancel := context.WithCancel(context.Background())
|
|
|
+ defer cancel()
|
|
|
+
|
|
|
+ incoming := make(chan *ipn.Notify, 2)
|
|
|
+ go func() {
|
|
|
+ incoming <- nm1
|
|
|
+ waitSend()
|
|
|
+ incoming <- nm2
|
|
|
+ waitSend()
|
|
|
+ st.advance(5 * time.Second)
|
|
|
+ select {
|
|
|
+ case n := <-flushc:
|
|
|
+ if n.NetMap != nm2.NetMap {
|
|
|
+ t.Errorf("got wrong NetMap; got %p", n.NetMap)
|
|
|
+ }
|
|
|
+ case <-time.After(10 * time.Second):
|
|
|
+ t.Error("timeout")
|
|
|
+ }
|
|
|
+ cancel()
|
|
|
+ }()
|
|
|
+
|
|
|
+ st.s.Run(ctx, incoming)
|
|
|
+ })
|
|
|
+}
|