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 看输出,现在都有真实断言。
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
package gobridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// testGoService 对应 example/main.go 里的 goService,实现 Handler 接口,
|
||||
// 公开方法自动暴露给 Python 通过 call_go() 调用。用于覆盖 example/main.go
|
||||
// 的 demoServer 演示过的 WithHandlers/call_go 全双工功能。
|
||||
type testGoService struct {
|
||||
pool Pool // 供 EnrichName 内部再调 Python
|
||||
|
||||
mu sync.Mutex
|
||||
logLines []string
|
||||
}
|
||||
|
||||
func (s *testGoService) Multiply(ctx context.Context, a, b int) (int, error) {
|
||||
return a * b, nil
|
||||
}
|
||||
|
||||
func (s *testGoService) Log(msg string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.logLines = append(s.logLines, msg)
|
||||
}
|
||||
|
||||
func (s *testGoService) takeLogLines() []string {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
lines := s.logLines
|
||||
s.logLines = nil
|
||||
return lines
|
||||
}
|
||||
|
||||
// EnrichName 内部通过 Invoke 调用 Python 的 to_upper,构成 Go→Python→Go→Python 四层链路。
|
||||
func (s *testGoService) EnrichName(ctx context.Context, name string) (string, error) {
|
||||
upper, err := Invoke[string](ctx, s.pool, "to_upper", name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "Hello, " + upper + "!", nil
|
||||
}
|
||||
|
||||
func (s *testGoService) MakeUser(ctx context.Context, uid int) (testUser, error) {
|
||||
return testUser{ID: uid, Name: fmt.Sprintf("user_%d", uid), Score: float64(uid) * 1.5}, nil
|
||||
}
|
||||
|
||||
func newTestServerPool(t *testing.T, svc *testGoService) Pool {
|
||||
t.Helper()
|
||||
pool := newTestPool(t, WithWorkers(1), WithHandlers(svc))
|
||||
svc.pool = pool
|
||||
return pool
|
||||
}
|
||||
|
||||
// TestHandlers 覆盖 example/main.go 的 demoServer 演示过的 WithHandlers/call_go
|
||||
// 全双工功能:Python 调用 Go 方法、流式输出过程中回调 Go、Go→Python→Go→Python
|
||||
// 四层链路、call_go[T] 自动构造 dataclass。
|
||||
func TestHandlers(t *testing.T) {
|
||||
svc := &testGoService{}
|
||||
pool := newTestServerPool(t, svc)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("Python调用Go方法", func(t *testing.T) {
|
||||
result, err := Invoke[int](ctx, pool, "compute_with_go_mul", 6, 7)
|
||||
if err != nil {
|
||||
t.Fatalf("Invoke: %v", err)
|
||||
}
|
||||
if result != 42 {
|
||||
t.Fatalf("want 42, got %d", result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("流式输出过程中回调Go", func(t *testing.T) {
|
||||
ch, err := Invoke[chan int](ctx, pool, "squared_with_log", 4)
|
||||
if err != nil {
|
||||
t.Fatalf("Invoke: %v", err)
|
||||
}
|
||||
var got []int
|
||||
for v := range ch {
|
||||
got = append(got, v)
|
||||
}
|
||||
want := []int{1, 4, 9, 16}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("want %v, got %v", want, got)
|
||||
}
|
||||
|
||||
wantLogs := []string{
|
||||
"yielding 1² = 1",
|
||||
"yielding 2² = 4",
|
||||
"yielding 3² = 9",
|
||||
"yielding 4² = 16",
|
||||
}
|
||||
if gotLogs := svc.takeLogLines(); !reflect.DeepEqual(gotLogs, wantLogs) {
|
||||
t.Fatalf("want log lines %v, got %v", wantLogs, gotLogs)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("四层全双工链路", func(t *testing.T) {
|
||||
// full_chain("world") → call_go[str]("EnrichName","world")
|
||||
// → Invoke[string](ctx, serv, "to_upper", "world") → "WORLD"
|
||||
// ← "Hello, WORLD!"
|
||||
greeting, err := Invoke[string](ctx, pool, "full_chain", "world")
|
||||
if err != nil {
|
||||
t.Fatalf("Invoke: %v", err)
|
||||
}
|
||||
if greeting != "Hello, WORLD!" {
|
||||
t.Fatalf("want %q, got %q", "Hello, WORLD!", greeting)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("call_go自动构造dataclass", func(t *testing.T) {
|
||||
enriched, err := Invoke[testUser](ctx, pool, "get_user_via_go", 12)
|
||||
if err != nil {
|
||||
t.Fatalf("Invoke: %v", err)
|
||||
}
|
||||
want := testUser{ID: 12, Name: "user_12", Score: 18, Level: "gold"}
|
||||
if enriched != want {
|
||||
t.Fatalf("want %+v, got %+v", want, enriched)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user