package xturbo import ( "errors" "os" "strings" "testing" "time" "golang.org/x/sys/unix" ) const shortDeadline = 150 * time.Millisecond // within runs f and fails unless it returns an error wrapping // os.ErrDeadlineExceeded after about d (not much later). func within(t *testing.T, d time.Duration, f func() error) { t.Helper() start := time.Now() err := f() took := time.Since(start) if !errors.Is(err, os.ErrDeadlineExceeded) { t.Fatalf("err %v, want a deadline error", err) } if took < d || took > d+2*time.Second { t.Fatalf("returned after %v, deadline %v", took, d) } } // A server that accepts the connection but never answers the setup request. func TestNewConnSetupDeadline(t *testing.T) { shortTimeouts(t, shortDeadline) fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) if err != nil { t.Fatal(err) } defer unix.Close(fds[1]) within(t, shortDeadline, func() error { c, err := NewConn(fds[0]) if c != nil { t.Fatal("conn") } return err }) if _, err := unix.FcntlInt(uintptr(fds[0]), unix.F_GETFD, 0); err != unix.EBADF { t.Fatal("fd not closed") } } // A server that completes the setup, then hangs during the bootstrap. func TestNewConnBootstrapDeadline(t *testing.T) { shortTimeouts(t, shortDeadline) sc := &script{} sc.silent.Store(true) fd, _ := startScript(t, sc) within(t, shortDeadline, func() error { _, err := NewConn(fd); return err }) } func TestSyncDeadline(t *testing.T) { sc := &script{exts: fullExts()} c, _ := dialScript(t, sc) sc.silent.Store(true) c.timeout = shortDeadline within(t, shortDeadline, func() error { return c.Sync(nil) }) within(t, shortDeadline, func() error { return c.InternAtoms([]string{"A"}, false, make([]uint32, 1)) }) c.xids = xidRange{} within(t, shortDeadline, func() error { _, err := c.NewXID(); return err }) } // A server flooding events but never answering the fence still times out. func TestSyncDeadlineFlood(t *testing.T) { sc := &script{} c, srv := dialScript(t, sc) c.timeout = shortDeadline sc.silent.Store(true) // Events carry the last request the server processed: the bootstrap's. ev := pktEvent(19, uint16(c.seq), 0) stop := make(chan struct{}) done := make(chan struct{}) go func() { defer close(done) for { select { case <-stop: return default: // Written raw: the silent server goroutine owns srv.seq. if _, err := srv.f.Write(ev); err != nil { return } } } }() // Closing the client unblocks a writer stuck on a full socket buffer. defer func() { close(stop); _ = c.Close(); <-done }() within(t, shortDeadline, func() error { return c.Sync(nil) }) } // A GetXIDRange answer whose last ID overflows the mask (into other clients' // IDs) is rejected. func TestXIDRangeBounds(t *testing.T) { c, _ := dialScript(t, &script{}) // Recorded setup: base 0x00200000, mask 0x001fffff (inc 1). for _, tc := range []struct { start, count uint32 ok bool }{ {0x00200000, 0xffffffff, false}, {0x00200000, 0x00200001, false}, {0x00200000, 0x00200000, true}, {0x003fffff, 1, true}, {0x003fffff, 2, false}, {0x00400000, 1, false}, // other client's base {0, 1, false}, {0x00200040, 0, false}, } { if got := c.validXIDRange(tc.start, tc.count); got != tc.ok { t.Errorf("{%#x, %#x}: %v", tc.start, tc.count, got) } } // A mask with inc > 1: the start must be aligned. c.xidMask = 0x001ffff0 if c.validXIDRange(0x00200008, 1) || !c.validXIDRange(0x00200010, 2) || c.validXIDRange(0x001ffff0|0x00200000, 2) { t.Fatal("inc 16") } } func TestNewXIDRejectsOverflowingRange(t *testing.T) { sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0x00200000, 0xffffffff}}} c, _ := dialScript(t, sc) c.xids = xidRange{} if _, err := c.NewXID(); !errors.Is(err, errXIDsExhausted) { t.Fatal(err) } } // XIDsLow + PrefetchXIDs let a callback allocate past the end of the current // range without blocking. func TestPrefetchXIDsFromCallback(t *testing.T) { sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0x00200100, 3}}} sc.hook = func(s *scriptConn, major, _ uint8, _ uint16, _ []byte) bool { if major == 8 { s.event(19, 0) return true } return false } c, srv := dialScript(t, sc) c.xids = xidRange{next: 0x10, left: 2} if !c.XIDsLow() { t.Fatal("not low") } var got []uint32 var errs []error h := Handler{OnEvent: func(*Event) { // Inside a callback: the prefetch is queued, no wait. if c.XIDsLow() { if err := c.PrefetchXIDs(); err != nil { errs = append(errs, err) } } }} c.Buf().Request(8, 0, 4) if err := c.Sync(&h); err != nil { t.Fatal(err) } if len(errs) != 0 || c.xidPend.left != 3 || c.XIDsLow() { t.Fatal(errs, c.xidPend, c.XIDsLow()) } // Second event: allocate 5 ids inside the callback, crossing ranges. h.OnEvent = func(*Event) { for range 5 { id, err := c.NewXID() if err != nil { errs = append(errs, err) return } got = append(got, id) } // Now everything is used; NewXID cannot block here. if _, err := c.NewXID(); !errors.Is(err, errReentrantDispatch) { errs = append(errs, err) } } c.Buf().Request(8, 0, 4) if err := c.Sync(&h); err != nil { t.Fatal(err) } want := []uint32{0x00200010, 0x00200011, 0x00200100, 0x00200101, 0x00200102} if len(errs) != 0 || len(got) != 5 { t.Fatal(errs, got) } for i := range want { if got[i] != want[i] { t.Fatalf("%#x", got) } } var ranges int for _, r := range srv.requests() { if r.major == 136 && r.minor == minorGetXIDRange { ranges++ } } if ranges != 1 { t.Fatal("GetXIDRange requests", ranges) } } func TestPrefetchXIDsStates(t *testing.T) { sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0, 1}}} c, srv := dialScript(t, sc) if c.XIDsLow() { t.Fatal("fresh setup range reported low") } c.xids = xidRange{next: 0x10, left: 1} before := len(srv.requests()) // Twice: one request only while in flight. first, second := c.PrefetchXIDs(), c.PrefetchXIDs() if first != nil || second != nil || c.XIDsLow() { t.Fatal("prefetch", first, second) } if err := c.Sync(nil); err != nil { t.Fatal(err) } if n := len(srv.requests()) - before; n != 2 { // GetXIDRange + fence t.Fatal(n) } // The server had none: not low (nothing to gain), current id still works, // then NewXID's blocking refill reports exhaustion. if c.XIDsLow() { t.Fatal("low after a refused prefetch") } if _, err := c.NewXID(); err != nil { t.Fatal(err) } if _, err := c.NewXID(); !errors.Is(err, errXIDsExhausted) { t.Fatal(err) } // Without XC-MISC. c2, _ := dialScript(t, &script{}) c2.xids = xidRange{} if c2.XIDsLow() || !errors.Is(c2.PrefetchXIDs(), ErrExtMissing) { t.Fatal("no XC-MISC") } } // A blocking refill waits for a prefetch already in flight instead of sending // a second request; an X error on it is returned by NewXID, not OnError. func TestRefillWaitsForPrefetch(t *testing.T) { sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0x00200200, 4}}} c, srv := dialScript(t, sc) c.xids = xidRange{} if err := c.PrefetchXIDs(); err != nil { t.Fatal(err) } id, err := c.NewXID() if err != nil || id != 0x00200200 { t.Fatalf("%#x %v", id, err) } var ranges int for _, r := range srv.requests() { if r.major == 136 && r.minor == minorGetXIDRange { ranges++ } } if ranges != 1 { t.Fatal(ranges) } esc := &script{exts: fullExts()} esc.hook = func(s *scriptConn, major, minor uint8, _ uint16, _ []byte) bool { if major == 136 && minor == minorGetXIDRange { s.xerror(11, major, 0) return true } return false } ec, _ := dialScript(t, esc) var userErrs int h := Handler{OnError: func(*XError) { userErrs++ }} ec.xids = xidRange{} if err := ec.PrefetchXIDs(); err != nil { t.Fatal(err) } if err := ec.Sync(&h); err != nil { t.Fatal("a prefetch error leaked into Sync:", err) } var xe *XError if _, err := ec.NewXID(); !errors.As(err, &xe) || xe.Kind != kindXIDRange || userErrs != 0 { t.Fatal(err, userErrs) } } // Steady state: XIDsLow does not allocate. func TestAllocXIDsLow(t *testing.T) { c, _ := dialScript(t, &script{exts: fullExts()}) if n := testing.AllocsPerRun(1000, func() { _ = c.XIDsLow() }); n != 0 { t.Fatal(n) } } // An over-long name is a caller bug: ErrBadLength, nothing queued, the Buf // still usable. func TestInternAtomsNameTooLong(t *testing.T) { c, _ := dialScript(t, &script{}) next := c.Buf().Next() err := c.InternAtoms([]string{"OK", strings.Repeat("x", 0x10000)}, false, make([]uint32, 2)) if !errors.Is(err, ErrBadLength) || c.Buf().Err() != nil || c.Buf().Next() != next { t.Fatal(err, c.Buf().Err()) } dst := make([]uint32, 1) if err := c.InternAtoms([]string{"OK"}, false, dst); err != nil || dst[0] == 0 { t.Fatal(err) } } // 0xffff requests queued without a Flush reach ErrSeqOverflow (the auto-resync // fence needs a Flush to be appended); with a Flush every so often the fence // keeps the stream going far past 0xffff. func TestAutoResyncVsSeqOverflow(t *testing.T) { c, _ := dialScript(t, &script{}) b := c.Buf() for b.Err() == nil { b.Request(8, 0, 4) } if !errors.Is(b.Err(), ErrSeqOverflow) || b.Next()-b.acked != maxSeqAhead { t.Fatal(b.Err(), b.Next()-b.acked) } if c.Stats().Resyncs != 0 { t.Fatal(c.Stats()) } c2, _ := dialScript(t, &script{}) b2 := c2.Buf() for i := range 3 * 0x10000 { b2.Request(8, 0, 4) if i%4096 == 0 { if err := flushAll(c2); err != nil { t.Fatal(err) } if err := c2.Dispatch(nil); err != nil { t.Fatal(err) } // Give the fence's reply time to come back before overflow. if b2.Next()-b2.acked > 0xe000 { dispatchFor(t, c2, func() bool { return b2.Next()-b2.acked < 0x9000 }) } } } if b2.Err() != nil { t.Fatal(b2.Err()) } if err := c2.Sync(nil); err != nil { t.Fatal(err) } if st := c2.Stats(); st.Resyncs < 3 || st.DroppedReplies != 0 { t.Fatalf("%+v", st) } }