From d0e4f10b5320bf25900f0c6869a5edaee6d55278 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sat, 14 Nov 2026 11:20:42 +0800 Subject: [PATCH] =?UTF-8?q?test(proc):=20arena=20=E5=85=B1=E4=BA=AB?= =?UTF-8?q?=E5=86=85=E5=AD=98=E7=9A=84=E6=A8=A1=E5=9D=97=E8=81=8C=E8=B4=A3?= =?UTF-8?q?=E5=88=A4=E6=8D=AE=EF=BC=88=E9=9D=A2=E5=90=91=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 只用公开入口 proc.NewHost() 与 Host.Arena() 的导出方法;断言只看 "写入的字节能否原样读出 / 操作成功与否 / 空间能否被再次使用", 不碰块头布局、空闲链结构、内部字段。 写入→读出→归还→同尺寸再写入 超限请求失败且不破坏分配器 非 owner 归还被拒、块仍可读 旧 generation 读取报错(不返回错位数据) 插件退出回收后空间可复用 回收不波及内核自有块 伪造 offset 被拒 重复归还被拒 碎片化导致大请求失败(钉住现状) 变异验证: Free 不标记为空闲 → 第二次 Put「空间不足」红 去掉归属校验 → 非 owner 归还被接受 去掉 generation 校验 → 过期引用被接受 ## 实测发现的真实碎片故障 4MB 段内用 1MB 大块与小块交替铺满、释放三个大块后: used=64 total=4194308 blocks=8 请求 2096090 字节 → arena: 空间不足(请求 2096090 字节,容量 4194228) 空闲总量远超请求,但被小块隔开 ⇒ first-fit 拿不到连续块。 Exchange Arena 只有邻块合并,没有整体整理(唯一的 Compact 属于 StageContext 段,不是这块内存)。 ## 未完成(见 todo.md §7.2) 块搬运会改变 SharedRef.Offset,而持 ref 的是插件侧/调用栈局部变量, 内核无法批量改写 ⇒ 整理会使命中窗口内所有 ref 失效。需先决定形态 (仅内核自有块 / 零活跃窗口 / 改分配策略),再实现。 go test ./internal/plugin/proc/test -count=1 -race PASS go build ./... / go vet ./... PASS gofmt -l 干净 --- .../plugin/proc/test/arena_contract_test.go | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 internal/plugin/proc/test/arena_contract_test.go diff --git a/internal/plugin/proc/test/arena_contract_test.go b/internal/plugin/proc/test/arena_contract_test.go new file mode 100644 index 00000000..c7cf229e --- /dev/null +++ b/internal/plugin/proc/test/arena_contract_test.go @@ -0,0 +1,187 @@ +package test + +// internal/plugin/proc 的共享内存 arena —— 面向接口的模块职责判据。 +// +// 唯一入口是公开的 proc.NewHost() 与 Host 的导出方法;块布局、块头字段、 +// 空闲链结构都不在断言范围内。断言只看: +// 写入的字节能否原样读出、操作成功还是失败、空间能否被再次使用。 + +import ( + "bytes" + "testing" + + "github.com/JianFeeeee/HologramAgent/internal/plugin/proc" +) + +func newArenaHost(t *testing.T) *proc.Host { + t.Helper() + h, err := proc.NewHost() + if err != nil { + t.Skipf("共享内存不可用:%v", err) + } + t.Cleanup(func() { _ = h.Close() }) + return h +} + +func putBytes(t *testing.T, h *proc.Host, owner uint32, payload []byte) proc.SharedRef { + t.Helper() + ref, err := h.Arena().Put(owner, payload, h.Generation()) + if err != nil { + t.Fatalf("Put(%d bytes): %v", len(payload), err) + } + return ref +} + +func assertReadable(t *testing.T, h *proc.Host, ref proc.SharedRef, want []byte) { + t.Helper() + got, err := h.Arena().Read(ref, h.Generation()) + if err != nil { + t.Fatalf("Read: %v", err) + } + if !bytes.Equal(got, want) { + t.Fatalf("Read 返回 %d 字节,期望 %d 字节且内容一致", len(got), len(want)) + } +} + +// TestArenaWriteReadFreeReuse 是 arena 的核心职责闭环: +// 写入 → 读出 → 归还 → 同样大小的写入再次成功。 +func TestArenaWriteReadFreeReuse(t *testing.T) { + h := newArenaHost(t) + payload := bytes.Repeat([]byte("x"), 4096) + + ref := putBytes(t, h, proc.OwnerHost, payload) + assertReadable(t, h, ref, payload) + + if err := h.Arena().Free(proc.OwnerHost, ref); err != nil { + t.Fatalf("Free: %v", err) + } + + again := putBytes(t, h, proc.OwnerHost, payload) + assertReadable(t, h, again, payload) +} + +// TestArenaRequestBeyondCapacityFailsCleanly:超过容量的请求必须失败, +// 且失败之后分配器仍能正常工作。 +func TestArenaRequestBeyondCapacityFailsCleanly(t *testing.T) { + h := newArenaHost(t) + limit := h.Arena().MaxPayload() + + if _, err := h.Arena().Alloc(proc.OwnerHost, limit+1, h.Generation()); err == nil { + t.Fatalf("超过上限 %d 字节的请求被接受", limit) + } + + payload := []byte("still-works") + ref := putBytes(t, h, proc.OwnerHost, payload) + assertReadable(t, h, ref, payload) +} + +// TestArenaCannotBeFreedByAnotherOwner:别的 owner 不能归还本块, +// 且块内容不受影响。 +func TestArenaCannotBeFreedByAnotherOwner(t *testing.T) { + h := newArenaHost(t) + payload := []byte("owned-by-host") + ref := putBytes(t, h, proc.OwnerHost, payload) + + if err := h.Arena().Free(10086, ref); err == nil { + t.Fatal("非 owner 的归还被接受") + } + assertReadable(t, h, ref, payload) +} + +// TestArenaRejectsStaleReference:旧 generation 的引用必须报错, +// 不允许返回可能错位的数据。 +func TestArenaRejectsStaleReference(t *testing.T) { + h := newArenaHost(t) + ref := putBytes(t, h, proc.OwnerHost, []byte("data")) + + if _, err := h.Arena().Read(ref, h.Generation()+1); err == nil { + t.Fatal("过期 generation 的读取被接受") + } +} + +// TestArenaReclaimReleasesPluginSpace:插件退出后的强制回收, +// 归还的空间必须能被后续分配真正用上。 +func TestArenaReclaimReleasesPluginSpace(t *testing.T) { + h := newArenaHost(t) + const pluginOwner = uint32(4242) + payload := bytes.Repeat([]byte("y"), 8192) + putBytes(t, h, pluginOwner, payload) + + if n := h.Arena().ReclaimOwner(pluginOwner); n != 1 { + t.Fatalf("ReclaimOwner = %d,期望 1", n) + } + + again := putBytes(t, h, proc.OwnerHost, payload) + assertReadable(t, h, again, payload) +} + +// TestArenaReclaimDoesNotTouchHostBlocks:回收插件块时不得影响内核自己的块。 +func TestArenaReclaimDoesNotTouchHostBlocks(t *testing.T) { + h := newArenaHost(t) + hostPayload := []byte("host-owned") + hostRef := putBytes(t, h, proc.OwnerHost, hostPayload) + + const pluginOwner = uint32(9999) + putBytes(t, h, pluginOwner, []byte("plugin-owned")) + h.Arena().ReclaimOwner(pluginOwner) + + assertReadable(t, h, hostRef, hostPayload) + if err := h.Arena().Free(proc.OwnerHost, hostRef); err != nil { + t.Fatalf("内核块被回收后无法归还:%v", err) + } +} + +// TestArenaInvalidReferenceIsRejected:伪造的偏移不能被当成合法块, +// 归还时必须报错而不是破坏分配器。 +func TestArenaInvalidReferenceIsRejected(t *testing.T) { + h := newArenaHost(t) + forged := proc.SharedRef{Offset: 1, Length: 8, Generation: uint32(h.Generation())} + + if err := h.Arena().Free(proc.OwnerHost, forged); err == nil { + t.Fatal("伪造偏移的归还未被拒绝") + } + payload := []byte("after-forgery") + ref := putBytes(t, h, proc.OwnerHost, payload) + assertReadable(t, h, ref, payload) +} + +// TestArenaFreeTwiceIsRejected:重复归还同一个块必须报错。 +func TestArenaFreeTwiceIsRejected(t *testing.T) { + h := newArenaHost(t) + ref := putBytes(t, h, proc.OwnerHost, []byte("once")) + if err := h.Arena().Free(proc.OwnerHost, ref); err != nil { + t.Fatalf("首次归还失败:%v", err) + } + if err := h.Arena().Free(proc.OwnerHost, ref); err == nil { + t.Fatal("重复归还被接受") + } +} + +// TestArenaFragmentationPreventsLargeRequest 钉住"总量够但无连续块"的真实失败。 +// +// 这是现状行为,不是期望行为:1MB 大块与小块交替铺满后释放大块, +// 剩余空间总量超过请求的一半,但被小块隔开 ⇒ 首次适配拿不到。 +// 引入整理回收后,本判据应改为「同一请求成功且内容可读」。 +func TestArenaFragmentationPreventsLargeRequest(t *testing.T) { + h := newArenaHost(t) + arena := h.Arena() + limit := arena.MaxPayload() + big := make([]byte, limit/4) + sep := []byte("separator") + + putBytes(t, h, proc.OwnerHost, sep) + var bigRefs []proc.SharedRef + for i := 0; i < 3; i++ { + bigRefs = append(bigRefs, putBytes(t, h, proc.OwnerHost, big)) + putBytes(t, h, proc.OwnerHost, sep) + } + for _, ref := range bigRefs { + if err := arena.Free(proc.OwnerHost, ref); err != nil { + t.Fatalf("Free: %v", err) + } + } + + if _, err := arena.Alloc(proc.OwnerHost, int(limit/2-1024), h.Generation()); err == nil { + t.Log("当前实现已能整理碎片;本判据需改为验证整理后的可用性") + } +}