test(proc): arena 共享内存的模块职责判据(面向接口)

只用公开入口 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                                            干净
This commit is contained in:
JianFeeeee
2026-11-14 11:20:42 +08:00
parent b73ff3786a
commit d0e4f10b53

View File

@ -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("当前实现已能整理碎片;本判据需改为验证整理后的可用性")
}
}