package rdpgfx import ( "bytes" "encoding/binary" "testing" ) // fakeStore 记录 Persist 调用并回放固定条目,用于验证持久缓存桥。 type fakeStore struct { persisted []GfxCacheEntry export []GfxCacheEntry } func (s *fakeStore) Persist(key uint64, w, h int, bpp uint16, data []byte) { cp := make([]byte, len(data)) copy(cp, data) s.persisted = append(s.persisted, GfxCacheEntry{Key: key, Width: w, Height: h, Bpp: bpp, Data: cp}) } func (s *fakeStore) Export() []GfxCacheEntry { return s.export } func (s *fakeStore) Get(key uint64) (GfxCacheEntry, bool) { for _, e := range s.export { if e.Key == key { return e, true } } return GfxCacheEntry{}, false } func (s *fakeStore) Keys() []uint64 { out := make([]uint64, 0, len(s.export)) for _, e := range s.export { out = append(out, e.Key) } return out } // newCacheTestHandler 返回带捕获 sendFn 的最小处理器(不经 NewGfxHandler, // 避免拉起解码/写循环 goroutine)。 func newCacheTestHandler() (*GfxHandler, *[][]byte) { sent := &[][]byte{} g := &GfxHandler{ surfaces: make(map[uint16]*surface), cacheEntries: make(map[uint16]cacheEntry), sendFn: func(b []byte) { cp := make([]byte, len(b)) copy(cp, b) *sent = append(*sent, cp) }, } return g, sent } func TestSendCacheImportOffer(t *testing.T) { g, sent := newCacheTestHandler() st := &fakeStore{export: []GfxCacheEntry{ {Key: 0x1122334455667788, Width: 8, Height: 2, Data: bytes.Repeat([]byte{0xAB}, 8*2*4)}, {Key: 0x0102030405060708 >> 0, Width: 4, Height: 4, Data: bytes.Repeat([]byte{0xCD}, 4*4*4)}, }} g.cacheStore = st // 空上报守卫:未调用 Export 前直接发送应只发一次,重复调用被闩住 g.sendCacheImportOffer() if len(*sent) != 1 { t.Fatalf("期望 1 条 PDU,实得 %d", len(*sent)) } g.sendCacheImportOffer() if len(*sent) != 1 { t.Fatalf("importOfferSent 闩失效:实得 %d 条", len(*sent)) } pdu := (*sent)[0] if got := binary.LittleEndian.Uint16(pdu[0:]); got != cmdidCacheImportOffer { t.Fatalf("cmdId=0x%X,期望 0x%X", got, cmdidCacheImportOffer) } wantLen := 8 + 2 + 12*2 if got := binary.LittleEndian.Uint32(pdu[4:]); int(got) != wantLen { t.Fatalf("pduLength=%d,期望 %d", got, wantLen) } if got := binary.LittleEndian.Uint16(pdu[8:]); got != 2 { t.Fatalf("cacheEntriesCount=%d,期望 2", got) } // 条目 1:key u64 + bitmapLength u32 if got := binary.LittleEndian.Uint64(pdu[10:]); got != 0x1122334455667788 { t.Fatalf("entry0 key=0x%X", got) } if got := binary.LittleEndian.Uint32(pdu[18:]); got != 8*2*4 { t.Fatalf("entry0 bitmapLength=%d,期望 %d", got, 8*2*4) } if got := binary.LittleEndian.Uint64(pdu[22:]); got != 0x0102030405060708 { t.Fatalf("entry1 key=0x%X", got) } if len(g.offeredCache) != 2 { t.Fatalf("offeredCache 应保留 2 条待映射,实得 %d", len(g.offeredCache)) } } func TestSendCacheImportOfferEmpty(t *testing.T) { g, sent := newCacheTestHandler() g.cacheStore = &fakeStore{export: nil} g.sendCacheImportOffer() if len(*sent) != 0 { t.Fatalf("空存储不应发送 PDU") } g.cacheStore = nil g.importOfferSent = false g.sendCacheImportOffer() if len(*sent) != 0 { t.Fatalf("无 store 不应发送 PDU") } } func TestOnCacheImportReply(t *testing.T) { g, _ := newCacheTestHandler() e0 := GfxCacheEntry{Key: 0xA, Width: 4, Height: 2, Data: bytes.Repeat([]byte{1}, 4*2*4)} e1 := GfxCacheEntry{Key: 0xB, Width: 2, Height: 2, Data: bytes.Repeat([]byte{2}, 2*2*4)} g.offeredCache = []GfxCacheEntry{e0, e1} // 前缀语义:前 2 条导入,槽位 7 与 9 data := make([]byte, 2, 2+4) binary.LittleEndian.PutUint16(data, 2) data = binary.LittleEndian.AppendUint16(data, 7) data = binary.LittleEndian.AppendUint16(data, 9) g.onCacheImportReply(data) ce, ok := g.cacheEntries[7] if !ok || ce.key != 0xA || ce.width != 4 || ce.height != 2 || !bytes.Equal(ce.data, e0.Data) { t.Fatalf("槽位 7 条目不符: %+v", ce) } ce, ok = g.cacheEntries[9] if !ok || ce.key != 0xB || !bytes.Equal(ce.data, e1.Data) { t.Fatalf("槽位 9 条目不符: %+v", ce) } if g.offeredCache != nil { t.Fatalf("Reply 后 offeredCache 应清空") } } func TestOnCacheImportReplyClamp(t *testing.T) { g, _ := newCacheTestHandler() bad := GfxCacheEntry{Key: 0xC, Width: 4, Height: 2, Data: []byte{1, 2, 3}} // 长度不符 ok1 := GfxCacheEntry{Key: 0xD, Width: 2, Height: 2, Data: bytes.Repeat([]byte{3}, 2*2*4)} g.offeredCache = []GfxCacheEntry{bad, ok1} // n=5 超过上报数(截到 2);条目 0 长度不符必须被拒(防花屏) data := make([]byte, 2, 2+10) binary.LittleEndian.PutUint16(data, 5) for _, s := range []uint16{3, 4} { data = binary.LittleEndian.AppendUint16(data, s) } g.onCacheImportReply(data) if _, hit := g.cacheEntries[3]; hit { t.Fatalf("长度不符的条目不应入缓存") } if ce, hit := g.cacheEntries[4]; !hit || ce.key != 0xD { t.Fatalf("槽位 4 应为有效条目") } // 槽位数组截断:只有 1 个完整槽位 g2, _ := newCacheTestHandler() g2.offeredCache = []GfxCacheEntry{ok1, ok1} short := []byte{2, 0, 6, 0} // n=2 但只有 1 个槽位 g2.onCacheImportReply(short) if _, hit := g2.cacheEntries[6]; !hit { t.Fatalf("截断时应导入完整部分") } if len(g2.cacheEntries) != 1 { t.Fatalf("截断时不应导入缺失槽位,实得 %d 条", len(g2.cacheEntries)) } } func TestSurfaceToCachePersists(t *testing.T) { g, _ := newCacheTestHandler() st := &fakeStore{} g.cacheStore = st // 8×4 表面,每行像素值 = 行号(BGRA 同值) sw, sh := 8, 4 sdata := make([]byte, sw*sh*4) for row := range sh { for col := 0; col < sw; col++ { o := (row*sw + col) * 4 sdata[o], sdata[o+1], sdata[o+2], sdata[o+3] = byte(row), byte(row), byte(row), 0xFF } } g.surfaces[1] = &surface{width: uint16(sw), height: uint16(sh), data: sdata} key := uint64(0x1122334455667788) p := make([]byte, 0, 20) p = binary.LittleEndian.AppendUint16(p, 1) // surfId p = binary.LittleEndian.AppendUint64(p, key) // cacheKey p = binary.LittleEndian.AppendUint16(p, 3) // slot p = binary.LittleEndian.AppendUint16(p, 2) // left p = binary.LittleEndian.AppendUint16(p, 1) // top p = binary.LittleEndian.AppendUint16(p, 6) // right p = binary.LittleEndian.AppendUint16(p, 3) // bottom g.onSurfaceToCache(p) ce := g.cacheEntries[3] if ce.width != 4 || ce.height != 2 { t.Fatalf("缓存条目尺寸 %dx%d,期望 4x2", ce.width, ce.height) } if ce.key != key { t.Fatalf("缓存条目 key=0x%X,期望 0x%X", ce.key, key) } // 第一行来自表面第 1 行(值为 1),第二行来自第 2 行(值为 2) if ce.data[0] != 1 || ce.data[(4*1)*4] != 2 { t.Fatalf("缓存像素内容不符: [0]=%d [row1]=%d", ce.data[0], ce.data[(4*1)*4]) } if len(st.persisted) != 1 { t.Fatalf("Persist 应被调用 1 次,实得 %d", len(st.persisted)) } pv := st.persisted[0] if pv.Key != key || pv.Width != 4 || pv.Height != 2 || !bytes.Equal(pv.Data, ce.data) { t.Fatalf("持久化条目不符: %+v", pv) } // key=0 不持久化(视为无效键) p0 := append([]byte(nil), p...) binary.LittleEndian.PutUint64(p0[2:], 0) g.onSurfaceToCache(p0) if len(st.persisted) != 1 { t.Fatalf("key=0 不应触发 Persist,实得 %d", len(st.persisted)) } } func TestMaxCacheImportEntries(t *testing.T) { if maxCacheImportEntries >= 0x1556 { t.Fatalf("maxCacheImportEntries=%d 必须小于规范上限 0x1556", maxCacheImportEntries) } }