package xturbo import ( "encoding/binary" "errors" "testing" ) const kindGeom = kindUser + 1 func TestExpectUnsentSeq(t *testing.T) { c, _ := dispatchConn(t) c.buf.next = 5 // requests 1..4 were sent if err := c.Expect(4, kindGeom, 0, 0, 0, 0); err != nil { t.Fatal(err) } for _, seq := range []Seq{5, 6, 1 << 20} { if err := c.Expect(seq, kindGeom, 0, 0, 0, 0); !errors.Is(err, errExpectUnsent) { t.Fatalf("Expect(%d) = %v", seq, err) } } if c.live != 1 { t.Fatalf("live %d", c.live) } } func TestExpectReplyRouting(t *testing.T) { c, srv := dispatchConn(t) r := newRecorder(c) var args [4]uint32 r.h.OnReply = func(rp *Reply) { r.got = append(r.got, record{'r', rp.Seq, uint32(rp.Kind), nil}) args = rp.Args } if err := c.Expect(2, kindGeom, 10, 11, 12, 13); err != nil { t.Fatal(err) } if err := c.Expect(3, kindGeom+1, 0, 0, 0, 0); err != nil { t.Fatal(err) } if st := c.Stats(); st.MaxRingUse != 2 { t.Fatal(st) } // 1: unregistered reply, dropped. 2: registered. 3: registered no-reply // request, retired by the later reply 4, which is unregistered. stream := append(append(append(pktReply(1, 0), pktReply(2, 8)...), pktEvent(22, 3, 0)...), pktReply(4, 0)...) feed(t, c, srv, stream, &r.h) if len(r.got) != 2 || r.got[0].class != 'r' || r.got[0].seq != 2 || r.got[0].kind != uint32(kindGeom) || args != [4]uint32{10, 11, 12, 13} { t.Fatalf("%v %v", r.got, args) } if st := c.Stats(); st.DroppedReplies != 2 || c.live != 0 { t.Fatalf("%+v live %d", st, c.live) } // A second reply to the same seq (or one after completion) is dropped. feed(t, c, srv, pktReply(4, 0), &r.h) if c.Stats().DroppedReplies != 3 { t.Fatal(c.Stats()) } // Expect on a completed request, or seq 0, is ignored. if c.Expect(2, kindGeom, 0, 0, 0, 0) != nil || c.Expect(0, kindGeom, 0, 0, 0, 0) != nil || c.live != 0 { t.Fatal("stale Expect registered") } if err := c.Expect(9, kindDiscard, 0, 0, 0, 0); err == nil { t.Fatal("reserved kind accepted") } } func TestExpectErrorRouting(t *testing.T) { c, srv := dispatchConn(t) var got []XError h := Handler{OnError: func(e *XError) { got = append(got, *e) }} if err := c.Expect(5, kindGeom, 7, 0, 0, 1); err != nil { t.Fatal(err) } stream := append(append(pktError(3, 4, 14), pktError(3, 5, 14)...), pktError(140, 6, 138)...) feed(t, c, srv, stream, &h) if len(got) != 3 { t.Fatal(got) } if got[0].Kind != 0 || got[0].Args != [4]uint32{} || got[0].Seq != 4 { t.Fatalf("unregistered: %+v", got[0]) } if got[1].Kind != kindGeom || got[1].Args != [4]uint32{7, 0, 0, 1} || got[1].Seq != 5 || got[1].Major != 14 || got[1].Bad != 0xdead { t.Fatalf("registered: %+v", got[1]) } if got[2].Ext != ExtXFixes || got[2].Rel != 0 || got[2].Kind != 0 { t.Fatalf("extension: %+v", got[2]) } if c.live != 0 || c.Stats().UnhandledErrors != 0 { t.Fatal(c.live, c.Stats()) } // A nil OnError counts. feed(t, c, srv, pktError(3, 7, 14), nil) if st := c.Stats(); st.UnhandledErrors != 1 { t.Fatal(st) } } func TestReservedKindsConsumed(t *testing.T) { c, srv := dispatchConn(t) r := newRecorder(c) for i, k := range []uint16{kindDiscard, kindInternAtom, kindQueryExtension, kindXIDRange, kindBigreqEnable, 15} { if err := c.expect(Seq(i+1), k, [4]uint32{}); err != nil { t.Fatal(err) } } var stream []byte for i := range 6 { stream = append(stream, pktReply(uint16(i+1), 4)...) } feed(t, c, srv, stream, &r.h) if len(r.got) != 0 || c.live != 0 || c.Stats().DroppedReplies != 0 { t.Fatalf("%v live %d %+v", r.got, c.live, c.Stats()) } } func TestExpectRingFull(t *testing.T) { c, srv := dispatchConn(t) for seq := Seq(1); seq <= ringSize; seq++ { if err := c.Expect(seq, kindGeom, 0, 0, 0, 0); err != nil { t.Fatal(seq, err) } } if err := c.Expect(ringSize+1, kindGeom, 0, 0, 0, 0); !errors.Is(err, ErrRingFull) { t.Fatal(err) } // Re-registering the same seq overwrites. if err := c.Expect(1, kindGeom+2, 0, 0, 0, 0); err != nil || c.live != ringSize || c.Stats().MaxRingUse != ringSize { t.Fatal(err, c.live) } // An event with seq 1 does not free slot 1 (its reply may follow); one // with seq 2 does. feed(t, c, srv, pktEvent(22, 1, 0), nil) if !errors.Is(c.Expect(ringSize+1, kindGeom, 0, 0, 0, 0), ErrRingFull) { t.Fatal("slot freed early") } feed(t, c, srv, pktEvent(22, 2, 0), nil) if err := c.Expect(ringSize+1, kindGeom, 0, 0, 0, 0); err != nil || c.live != ringSize { t.Fatal(err, c.live) } // A jump far ahead retires every older slot. feed(t, c, srv, pktEvent(22, 0x9000, 0), nil) if c.live != 0 { t.Fatal(c.live) } } func TestAllocExpect(t *testing.T) { c, srv := dispatchConn(t) var sink uint32 h := Handler{ OnReply: func(r *Reply) { sink += r.Args[0] + uint32(r.Kind) }, OnError: func(e *XError) { sink += e.Args[0] + uint32(e.Kind) }, } // Each round: 3 registered requests, a reply, an error, a void request // retired by the next round's first packet. round := append(append(pktReply(0, 0), pktError(3, 0, 12)...), pktEvent(22, 0, 0)...) next := Seq(1) run := func() { for i := range Seq(3) { if err := c.Expect(next+i, kindGeom, uint32(i), 0, 0, 0); err != nil { t.Fatal(err) } binary.LittleEndian.PutUint16(round[32*i+2:], uint16(next+i)) } next += 3 feed(t, c, srv, round, &h) } run() if n := testing.AllocsPerRun(1000, run); n != 0 { t.Fatalf("Expect + reply/error match allocates %v times", n) } if c.Stats().MaxRingUse > 4 { t.Fatal(c.Stats()) } _ = sink } // FuzzReplyRing drives Expect and packet arrival against a map model. Each op // byte: low 2 bits choose Expect / reply / error / event, the rest a sequence // step. Invariants: Expect fails only with more than ringSize live // registrations of distinct incomplete seqs; each registered reply/error is // delivered once with its kind and args; live count matches the model. func FuzzReplyRing(f *testing.F) { f.Add([]byte{0, 4, 1, 8, 2, 0xff, 3, 0x40}) f.Add([]byte{0, 4, 8, 0xfd, 1, 0xfc, 1, 0xfc, 1, 0xfc, 1, 0xfc, 1, 0xfc, 1, 0xfc, 1, 0xfc, 1, 0xfc, 1, 0xfc, 1, 0xfc, 1, 0xfc, 1}) f.Add([]byte{0x80, 0x84, 0x88, 1, 5, 9, 3}) f.Fuzz(func(t *testing.T, ops []byte) { c, srv := dispatchConn(t) model := map[Seq]uint32{} // registered seq -> args[0] var gotSeq Seq var gotArg uint32 var gotKind uint16 delivered := 0 h := Handler{ OnReply: func(r *Reply) { gotSeq, gotArg, gotKind = r.Seq, r.Args[0], r.Kind; delivered++ }, OnError: func(e *XError) { gotSeq, gotArg, gotKind = e.Seq, e.Args[0], e.Kind; delivered++ }, } wire := Seq(0) // last sequence sent by the "server" for i, op := range ops { step := Seq(op >> 2) jump := step // replies may leap past the ring and the 16-bit wrap if step >= 56 { jump = (step - 55) * 700 } switch op & 3 { case 0: // Expect wire+1+step*13 seq := wire + 1 + step*13 err := c.Expect(seq, kindGeom, uint32(i), 0, 0, 0) clash := false for s := range model { if s != seq && s&ringMask == seq&ringMask { clash = true } } if clash != errors.Is(err, ErrRingFull) || (err != nil && !clash) { t.Fatalf("Expect(%d) = %v, clash %v", seq, err, clash) } if err == nil { model[seq] = uint32(i) } default: // a reply, error or event for wire+step seq := wire + jump if seq == 0 { seq = 1 } var p []byte switch op & 3 { case 1: p = pktReply(uint16(seq), 0) case 2: p = pktError(3, uint16(seq), 1) default: p = pktEvent(22, uint16(seq), 0) } delivered = 0 gotKind = 0 feed(t, c, srv, p, &h) if c.seq != seq { t.Fatalf("widened %d, want %d", c.seq, seq) } arg, reg := model[seq] switch op & 3 { case 1: if reg != (delivered == 1) || reg && (gotSeq != seq || gotArg != arg || gotKind != kindGeom) { t.Fatalf("reply %d: reg %v delivered %d", seq, reg, delivered) } case 2: if delivered != 1 || gotSeq != seq || reg != (gotKind == kindGeom) || reg && gotArg != arg { t.Fatalf("error %d: reg %v kind %d", seq, reg, gotKind) } } if op&3 != 3 { delete(model, seq) } for s := range model { if s < seq { delete(model, s) } } wire = seq } if c.live != len(model) || c.live > ringSize { t.Fatalf("live %d, model %d", c.live, len(model)) } if c.Stats().MaxRingUse > ringSize { t.Fatal(c.Stats()) } } }) }