本章回答:NewPrepared 返回后,哪些状态属于 Runner,哪些状态只能属于本轮 Guest?测试加载真实 dist/spine.wasm,先确认捕获阶段没有调用 Host 工具,再用普通 Run、Python 写入、两个活实例、PLM、prefix、Guest 错误和取消逐段检查边界。它验证的是本仓库的 prepared full-copy 生命周期,不是 allocator 的模拟,也不是把所有 Host 状态都纳入基线。
1. 测试入口、输入和时间边界
prepared_test.go · L1–L18 · 打开完整源码
package spine
import (
"context"
"encoding/json"
"os"
"strings"
"testing"
"time"
)
func TestPreparedGuest(t *testing.T) {
wasm, err := os.ReadFile("dist/spine.wasm")
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
测试使用 spine 包,导入上下文、JSON、artifact 文件、字符串、testing 和 time。TestPreparedGuest 直接读取 dist/spine.wasm;文件不存在就失败,因为没有真实 Guest 不能声称验证了 prepared 路径。两分钟上下文覆盖整个测试,返回时取消它。
这里的输入分成三层:Wasm 字节决定 Guest memory 形状;Manifest 注册 Host 工具;测试源码决定本轮 Guest 的用户写入。构造器只接收前两层,不接收任何用户请求。
2. 工具回调和“捕获时没有调用”
prepared_test.go · L19–L46 · 打开完整源码
calls := make(chan string, 32)
var second, prefixStarted chan struct{}
read := func(ctx context.Context, raw json.RawMessage) (any, error) {
var a struct {
Key string `json:"key"`
}
if err := json.Unmarshal(raw, &a); err != nil {
return nil, err
}
calls <- a.Key
switch a.Key {
case "first":
select {
case <-second:
return 21, nil
case <-ctx.Done():
return nil, ctx.Err()
}
case "second":
close(second)
return 5, nil
case "prefix":
close(prefixStarted)
return 21, nil
default:
return 21, nil
}
}
测试用有缓冲 calls 记录 Host 工具收到的 key,并预留 second、prefixStarted 两个握手 channel。read 先把 json.RawMessage 解码为带 key 的结构,再按 key 分支:first 等待 second 或上下文取消,second 关闭握手并返回 5,prefix 通知 prefix 流程并返回 21,其他 key 返回 21。
这些回调有两个用途。calls 是捕获阶段的观测点;两个并发 channel 则构造 PLM 和 prefix 的真实时序。它们不属于 Guest baseline,也没有被写入 r.image。调用者若在准备阶段误执行了 Host 工具,calls 会在构造器返回后留下记录。
3. 构造 Runner 后检查基线状态
prepared_test.go · L47–L63 · 打开完整源码
r, err := NewPrepared(ctx, wasm, Manifest{"lookup": {Call: read, AllowEarlyRead: true}})
if err != nil {
t.Fatal(err)
}
defer r.Close(context.Background())
if len(r.image) == 0 {
t.Fatal("missing actual initialized image")
}
if len(calls) != 0 {
t.Fatal("capture called a Host tool")
}
expect := func(t *testing.T, out Output, err error, want string) {
t.Helper()
if err != nil || string(out.Value) != want {
t.Fatalf("%+v %v; want %s", out, err, want)
}
}
测试以 Manifest{"lookup": {Call: read, AllowEarlyRead: true}} 调用 NewPrepared,成功后延迟关闭 Runner。这里的 lookup 是当前 Manifest 驱动的具名工具;测试没有通过旧式工具名参数绕过 manifest。
构造器返回后断言 len(r.image) != 0,说明 Runner 持有捕获的 Guest 线性内存字节;再断言 len(calls) == 0,说明参考 Guest 没有在捕获阶段走 Host bridge。测试只能据此证明这个注册工具未被调用,不能把它扩大成“所有外部资源都被捕获”。
expect 同时检查 Go error 和 Guest 返回的 JSON 值,后面的行为断言因此不会把“没有崩溃”误认为“结果正确”。此时没有活参考 Guest 暴露给测试;参考 module 的关闭由构造器内部完成。
4. 普通调用和请求级 Python 状态
prepared_test.go · L65–L74 · 打开完整源码
t.Run("ordinary tool", func(t *testing.T) {
out, err := r.Run(ctx, `result=lookup(key="book")`, nil)
expect(t, out, err, "21")
})
t.Run("private Python state", func(t *testing.T) {
out, err := r.Run(ctx, `import builtins; builtins.secret=123; result=builtins.secret`, nil)
expect(t, out, err, "123")
out, err = r.Run(ctx, `import builtins; result=hasattr(builtins,"secret")`, nil)
expect(t, out, err, "false")
})
第一条子测试执行 lookup(key="book"),检查真实 Host/Guest bridge 返回 21。第二条先在一轮中写 builtins.secret=123 并读回 123,再开启下一轮检查 hasattr 为 false。
状态序列是:
Runner.image:捕获后不变
Guest A:写入 builtins.secret,当前轮可读 123
Guest A 关闭
Guest B:从同一基线创建,不存在 secret
这个断言关注的是 Guest memory 与 Python runtime 状态的请求隔离。它不声称 Go Host globals 被复制进 image;Host 工具仍由 Runner 持有,调用期间只是把结果交付给当前 Guest。
5. 两个活 Guest 与 full-copy image 三方独立
prepared_test.go · L75–L96 · 打开完整源码
t.Run("two live memories and image are independent", func(t *testing.T) {
a, err := r.newGuest(ctx, &boundedText{}, &boundedText{})
if err != nil {
t.Fatal(err)
}
defer a.Close(context.Background())
b, err := r.newGuest(ctx, &boundedText{}, &boundedText{})
if err != nil {
t.Fatal(err)
}
defer b.Close(context.Background())
// Last byte is only a memory-isolation probe. These two instances execute no code afterward.
offset := uint32(len(r.image) - 1)
before := r.image[offset]
if !a.Memory().WriteByte(offset, before^255) {
t.Fatal("write")
}
value, ok := b.Memory().ReadByte(offset)
if !ok || value != before || r.image[offset] != before {
t.Fatal("shared writable storage")
}
})
测试直接调用两次 r.newGuest,让 A、B 同时存活,并在退出时关闭它们。它用 len(r.image)-1 选取基线最后一个字节作为内存探针;两个实例之后不再执行 Python,避免把这个低层探针和运行时语义混在一起。
A 把该字节从 before 改为 before^255。随后测试从 B 读取同一偏移,并再次读取 r.image,要求 B 和 image 都仍是 before。因此被区分的三个对象是:A 的私有 Guest memory、B 的另一份 Guest memory、Runner 拥有的 full-copy 基线。
探针不能证明每个字节都没有别的别名,也不能证明 Host 状态隔离;它只证明在这个 Guest memory 偏移上,A 的写入没有进入 B 或 Runner 的字节基线。
6. PLM:准备机制不改工具等待语义
prepared_test.go · L97–L104 · 打开完整源码
t.Run("PLM still overlaps", func(t *testing.T) {
second = make(chan struct{})
out, err := r.RunPLM(ctx, "a=lookup(key='first')\nb=lookup(key='second')\nresult=a+b", nil)
expect(t, out, err, "26")
if !strings.Contains(out.Transformed, "_spine_prepare") {
t.Fatal("PLM not used")
}
})
子测试重新创建 second channel,然后运行两个相邻工具赋值。first 必须等到 second 关闭才返回,因此 PLM 若按预期提前调度,结果是 21+5=26。测试还检查 out.Transformed 含 _spine_prepare,排除普通解释执行偶然得到同一数值的可能。
second 必须每次重新创建,因为回调会关闭它;关闭后的 channel 永远可读,再用同一 channel 会失去这次握手所表达的状态。该测试证明 prepared 只改变 Guest 创建/恢复,不替换 PLM 的 Host 协作。
7. Prefix:在 EOF 前已有 Guest 工作
prepared_test.go · L105–L130 · 打开完整源码
t.Run("prefix before EOF", func(t *testing.T) {
runCtx, stop := context.WithCancel(ctx)
defer stop()
prefixStarted = make(chan struct{})
chunks := make(chan string)
go func() {
defer close(chunks)
select {
case chunks <- "a=lookup(key='prefix')\n":
case <-runCtx.Done():
return
}
select {
case <-prefixStarted:
case <-runCtx.Done():
return
}
select {
case chunks <- "result=a\n":
case <-runCtx.Done():
return
}
}()
out, err := r.RunPrefix(runCtx, chunks, nil)
expect(t, out, err, "21")
})
测试为本轮建立可取消的 runCtx,创建 prefixStarted 和无缓冲 chunks。goroutine 先发送 a=lookup(key='prefix'),然后等待工具回调关闭 prefixStarted,再发送 result=a,最后关闭 chunks。RunPrefix 必须在收到第二段源码前已经让第一段进入 Guest 并调用工具,最终输出 21。
这个时序比一次性字符串更具体:第一段到达、工具执行、第二段到达、EOF,四个状态按 channel 握手排列。defer stop() 和发送方对 runCtx.Done() 的选择分支保证取消时不会把 goroutine 留在无接收者的发送上。
8. Guest 错误和取消后的 Runner 状态
prepared_test.go · L131–L147 · 打开完整源码
t.Run("error does not change image", func(t *testing.T) {
if _, err := r.Run(ctx, `raise ValueError("bad")`, nil); err == nil {
t.Fatal("missing error")
}
out, err := r.Run(ctx, `result=7`, nil)
expect(t, out, err, "7")
})
t.Run("cancel does not change image", func(t *testing.T) {
deadline, stop := context.WithTimeout(ctx, 2*time.Second)
defer stop()
if _, err := r.Run(deadline, `while True: pass`, nil); err == nil || deadline.Err() == nil {
t.Fatalf("expected deadline: %v", err)
}
out, err := r.Run(ctx, `result=7`, nil)
expect(t, out, err, "7")
})
}
错误子测试先运行会抛出 ValueError 的源码,再运行 result=7。前一次应返回错误,后一次仍应得到 7;错误 Guest 的 module 收尾不能改写 Runner.image,也不能让下一轮复用它的可写 memory。
取消子测试给无限循环设置两秒 deadline,要求 Run 返回错误且 deadline.Err() 已经有原因。测试随后再运行 result=7,检查取消只结束这一轮。defer 负责停止子上下文和关闭资源,测试函数退出后 Runner 也被关闭。
错误处理按生命周期分组观察:构造错误使测试失败;单轮 Guest 错误或 deadline 只影响该 module;只有所有 Run 结束后才关闭 Runner。测试没有把“返回 error”直接解释为“资源已经释放”,释放责任仍由各层 Close 和上下文协作完成。
9. 测试读完应能回答的状态问题
| 阶段 | Runner 级状态 | 当前 Guest 级状态 | 证据 |
|---|---|---|---|
NewPrepared 返回 |
image 非空,calls 为空 |
参考 Guest 已关闭 | 构造器检查 |
| 普通 Run | image 不变 | 本轮收到 21 | Host bridge |
| Python 写入 | image 不变 | A 可读 secret=123 |
同轮结果 |
| 下一轮 | 仍使用同一 image | B 看不到 secret |
跨轮断言 |
| 两个活实例 | image 未改 | A 写,B 不变 | 三方探针 |
| PLM / prefix | prepared 状态未改 | 现有执行路径继续工作 | channel 与输出 |
| 错误 / 取消后 | Runner 仍可创建 Guest | 失败轮结束 | 下一轮 7 |
COW 的同类状态还要分别通过内核映射测试和真实 Guest 测试,入口见 cow-kernel-tests.md 与 cow-guest-tests.md。