Files
gobridge/session_test.go
T
what ee5e5b96af feat: 添加 WithStreamErrors 查询流式调用执行过程中的异常
流式输出/双向流的 handler(Python 生成器)如果执行过程中抛异常,
Invoke[chan T] 本身的 err 只描述"调用有没有发起成功",跟这个异常
无关(永远是 nil),channel 只会静默提前关闭,调用方原本完全无法
感知。新增 WithStreamErrors(ctx) 返回一个包过的 ctx 和一个查询函数
streamErr,opt-in 之后可以查到具体错误。

错误记录挂在 WithStreamErrors 返回的 ctx 的对象图里(context.WithValue),
不是全局表——调用方不再引用 ctx/channel 时会被 GC 自然回收,不需要
任何显式清理逻辑,也不依赖 ctx.Done(),即使用 context.Background()
也能正常释放;ctx 之后被别的 context.With*(包括 StickyCtx)再包一层
也不影响查询。

同时补充完整的自动化测试覆盖 example/main.go 里演示过的所有功能:
四种调用模式 × int/struct/slice/[]byte 的组合(client_test.go)、
WithHandlers/call_go 全双工(handlers_test.go)、NewSession 隔离性
与 StickyCtx 路由(session_test.go),之前这些只能靠人肉跑 go run
看输出,现在都有真实断言。
2026-07-23 16:24:05 +08:00

150 lines
5.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package gobridge
import (
"context"
"fmt"
"reflect"
"testing"
)
// sessionResult 对应 example/worker.py 里 session_result 返回的
// {"value": int, "steps": [int, ...]}。
type sessionResult struct {
Value int `json:"value"`
Steps []int `json:"steps"`
}
// parseWorkerID 从 "[worker N] ..." 格式的字符串里解析出 worker 编号,
// example/worker.py 里 session_init/global_increment/global_get 都是这个格式。
func parseWorkerID(t *testing.T, msg string) int {
t.Helper()
var id int
if _, err := fmt.Sscanf(msg, "[worker %d]", &id); err != nil {
t.Fatalf("parse worker id from %q: %v", msg, err)
}
return id
}
// TestNewSessionIsolation 覆盖 example/main.go 的 demoSession 演示过的
// NewSession 功能:不同 session 之间状态互相隔离;同一个 worker 进程内的
// session 共享进程级全局变量,不同 worker 之间完全独立。
func TestNewSessionIsolation(t *testing.T) {
pool := newTestPool(t, WithWorkers(2))
ctx := context.Background()
// NewSession 内部按 pool 共享的原子计数器轮询:第一次 idx=1%2=1,
// 第二次 idx=2%2=0,第三次 idx=3%2=1——sessA 和 sessC 落在同一个 worker
// sessB 落在另一个(用全新 pool,不掺杂其它调用,保证这个顺序确定)。
sessA := NewSession(pool)
sessB := NewSession(pool)
sessC := NewSession(pool)
if _, err := Invoke[string](ctx, sessA, "session_init", "A", 100); err != nil {
t.Fatalf("sessA init: %v", err)
}
if _, err := Invoke[string](ctx, sessB, "session_init", "B", 200); err != nil {
t.Fatalf("sessB init: %v", err)
}
if _, err := Invoke[string](ctx, sessC, "session_init", "C", 300); err != nil {
t.Fatalf("sessC init: %v", err)
}
// 各自 step,验证会话状态互不干扰
if v, err := Invoke[int](ctx, sessA, "session_step", "A", 10); err != nil || v != 110 {
t.Fatalf("sessA step(+10): v=%d err=%v, want 110", v, err)
}
if v, err := Invoke[int](ctx, sessA, "session_step", "A", 5); err != nil || v != 115 {
t.Fatalf("sessA step(+5): v=%d err=%v, want 115", v, err)
}
if v, err := Invoke[int](ctx, sessB, "session_step", "B", 50); err != nil || v != 250 {
t.Fatalf("sessB step(+50): v=%d err=%v, want 250", v, err)
}
if v, err := Invoke[int](ctx, sessC, "session_step", "C", 99); err != nil || v != 399 {
t.Fatalf("sessC step(+99): v=%d err=%v, want 399(与 sessA 同 worker 但状态独立)", v, err)
}
rA, err := Invoke[sessionResult](ctx, sessA, "session_result", "A")
if err != nil {
t.Fatalf("sessA result: %v", err)
}
if want := (sessionResult{Value: 115, Steps: []int{10, 5}}); !reflect.DeepEqual(rA, want) {
t.Fatalf("sessA result: want %+v, got %+v", want, rA)
}
rB, err := Invoke[sessionResult](ctx, sessB, "session_result", "B")
if err != nil {
t.Fatalf("sessB result: %v", err)
}
if want := (sessionResult{Value: 250, Steps: []int{50}}); !reflect.DeepEqual(rB, want) {
t.Fatalf("sessB result: want %+v, got %+v", want, rB)
}
rC, err := Invoke[sessionResult](ctx, sessC, "session_result", "C")
if err != nil {
t.Fatalf("sessC result: %v", err)
}
if want := (sessionResult{Value: 399, Steps: []int{99}}); !reflect.DeepEqual(rC, want) {
t.Fatalf("sessC result: want %+v, got %+v", want, rC)
}
// 进程级全局变量:sessA 和 sessC 同一个 worker 进程,应该共享 _global_counter
// sessB 是独立进程,从 0 开始,不受影响。
rawA, err := Invoke[string](ctx, sessA, "global_increment", 10)
if err != nil {
t.Fatalf("sessA global_increment: %v", err)
}
workerA := parseWorkerID(t, rawA)
rawC, err := Invoke[string](ctx, sessC, "global_increment", 5)
if err != nil {
t.Fatalf("sessC global_increment: %v", err)
}
workerC := parseWorkerID(t, rawC)
rawB, err := Invoke[string](ctx, sessB, "global_increment", 99)
if err != nil {
t.Fatalf("sessB global_increment: %v", err)
}
workerB := parseWorkerID(t, rawB)
if workerA != workerC {
t.Fatalf("want sessA/sessC 落在同一个 workergot worker %d / %d", workerA, workerC)
}
if workerB == workerA {
t.Fatalf("want sessB 落在独立的 workergot 跟 sessA/sessC 一样都是 worker %d", workerB)
}
wantA := fmt.Sprintf("[worker %d] counter = 15", workerA) // sessA(+10) + sessC(+5) 共享
if rawA2, err := Invoke[string](ctx, sessA, "global_get"); err != nil || rawA2 != wantA {
t.Fatalf("sessA global_get: want %q, got %q (err=%v)", wantA, rawA2, err)
}
wantB := fmt.Sprintf("[worker %d] counter = 99", workerB) // 独立进程,只有自己的 +99
if rawB != wantB {
t.Fatalf("sessB global_increment: want %q, got %q", wantB, rawB)
}
}
// TestStickyCtx 覆盖 example/main.go 的 demoSession 演示过的 StickyCtx 功能:
// 相同的亲和 key 无论调用多少次,都应该稳定路由到同一个 worker 进程。
func TestStickyCtx(t *testing.T) {
pool := newTestPool(t, WithWorkers(2))
ctx := StickyCtx(context.Background(), "sticky-key")
var firstWorker int
for i := 0; i < 4; i++ {
msg, err := Invoke[string](ctx, pool, "session_init", fmt.Sprintf("aff-%d", i), i)
if err != nil {
t.Fatalf("Invoke #%d: %v", i, err)
}
worker := parseWorkerID(t, msg)
if i == 0 {
firstWorker = worker
continue
}
if worker != firstWorker {
t.Fatalf("StickyCtx 路由不稳定:第 1 次落在 worker %d,第 %d 次落在 worker %d", firstWorker, i+1, worker)
}
}
}