流式输出/双向流的 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 看输出,现在都有真实断言。
208 lines
5.0 KiB
Go
208 lines
5.0 KiB
Go
package gobridge
|
|
|
|
import (
|
|
"context"
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
// testUser 对应 example/worker.py 里的 User dataclass。
|
|
type testUser struct {
|
|
ID int `json:"id"`
|
|
Name string `json:"name"`
|
|
Score float64 `json:"score"`
|
|
Level string `json:"level,omitempty"`
|
|
}
|
|
|
|
// TestInvokeModes 覆盖 example/main.go 的 demoPool 演示过的四种调用模式
|
|
// (普通调用、流式输出、流式输入、双向流)在 int / struct / slice / []byte
|
|
// 各种类型组合下的正确性,对应 README「四种调用模式」小节。
|
|
func TestInvokeModes(t *testing.T) {
|
|
pool := newTestPool(t, WithWorkers(2), WithMaxConns(4))
|
|
ctx := context.Background()
|
|
|
|
t.Run("普通调用_int", func(t *testing.T) {
|
|
sum, err := Invoke[int](ctx, pool, "add", 3, 4)
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
if sum != 7 {
|
|
t.Fatalf("want 7, got %d", sum)
|
|
}
|
|
})
|
|
|
|
t.Run("流式输出_int", func(t *testing.T) {
|
|
ch, err := Invoke[chan int](ctx, pool, "range_gen", 1, 6)
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
var got []int
|
|
for v := range ch {
|
|
got = append(got, v)
|
|
}
|
|
want := []int{1, 2, 3, 4, 5}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("want %v, got %v", want, got)
|
|
}
|
|
})
|
|
|
|
t.Run("流式输入_int", func(t *testing.T) {
|
|
inputCh := make(chan int, 10)
|
|
go func() {
|
|
for i := 1; i <= 5; i++ {
|
|
inputCh <- i
|
|
}
|
|
close(inputCh)
|
|
}()
|
|
total, err := Invoke[int](ctx, pool, "sum_stream", inputCh)
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
if total != 15 {
|
|
t.Fatalf("want 15, got %d", total)
|
|
}
|
|
})
|
|
|
|
t.Run("双向流_int", func(t *testing.T) {
|
|
inputCh := make(chan int, 10)
|
|
go func() {
|
|
for i := 1; i <= 5; i++ {
|
|
inputCh <- i
|
|
}
|
|
close(inputCh)
|
|
}()
|
|
outCh, err := Invoke[chan int](ctx, pool, "double_stream", inputCh)
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
var got []int
|
|
for v := range outCh {
|
|
got = append(got, v)
|
|
}
|
|
want := []int{1, 4, 9, 16, 25}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("want %v, got %v", want, got)
|
|
}
|
|
})
|
|
|
|
t.Run("普通调用_struct", func(t *testing.T) {
|
|
user, err := Invoke[testUser](ctx, pool, "get_user", 42)
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
want := testUser{ID: 42, Name: "user_42", Score: 63}
|
|
if user != want {
|
|
t.Fatalf("want %+v, got %+v", want, user)
|
|
}
|
|
})
|
|
|
|
users := []testUser{
|
|
{ID: 1, Name: "alice", Score: 5.0},
|
|
{ID: 2, Name: "bob", Score: 8.0},
|
|
{ID: 3, Name: "carol", Score: 12.0},
|
|
}
|
|
|
|
t.Run("slice输入_返回标量", func(t *testing.T) {
|
|
scoreSum, err := Invoke[float64](ctx, pool, "total_score", users)
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
if scoreSum != 25 {
|
|
t.Fatalf("want 25, got %v", scoreSum)
|
|
}
|
|
})
|
|
|
|
t.Run("slice输入输出", func(t *testing.T) {
|
|
enriched, err := Invoke[[]testUser](ctx, pool, "enrich_users", users)
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
want := []testUser{
|
|
{ID: 1, Name: "alice", Score: 5.0, Level: "silver"},
|
|
{ID: 2, Name: "bob", Score: 8.0, Level: "silver"},
|
|
{ID: 3, Name: "carol", Score: 12.0, Level: "gold"},
|
|
}
|
|
if !reflect.DeepEqual(enriched, want) {
|
|
t.Fatalf("want %+v, got %+v", want, enriched)
|
|
}
|
|
})
|
|
|
|
t.Run("流式输出_struct", func(t *testing.T) {
|
|
userCh, err := Invoke[chan testUser](ctx, pool, "gen_users", 3)
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
var got []testUser
|
|
for u := range userCh {
|
|
got = append(got, u)
|
|
}
|
|
want := []testUser{
|
|
{ID: 1, Name: "user_1", Score: 3},
|
|
{ID: 2, Name: "user_2", Score: 6},
|
|
{ID: 3, Name: "user_3", Score: 9},
|
|
}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("want %+v, got %+v", want, got)
|
|
}
|
|
})
|
|
|
|
t.Run("双向流_struct", func(t *testing.T) {
|
|
inCh := make(chan testUser, len(users))
|
|
go func() {
|
|
for _, u := range users {
|
|
inCh <- u
|
|
}
|
|
close(inCh)
|
|
}()
|
|
procCh, err := Invoke[chan testUser](ctx, pool, "process_users", inCh)
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
var got []testUser
|
|
for u := range procCh {
|
|
got = append(got, u)
|
|
}
|
|
want := []testUser{
|
|
{ID: 1, Name: "ALICE", Score: 10},
|
|
{ID: 2, Name: "BOB", Score: 16},
|
|
{ID: 3, Name: "CAROL", Score: 24},
|
|
}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("want %+v, got %+v", want, got)
|
|
}
|
|
})
|
|
|
|
t.Run("bytes_输入输出", func(t *testing.T) {
|
|
rev, err := Invoke[[]byte](ctx, pool, "bytes_reverse", []byte("hello"))
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
if string(rev) != "olleh" {
|
|
t.Fatalf("want olleh, got %s", rev)
|
|
}
|
|
|
|
cat, err := Invoke[[]byte](ctx, pool, "bytes_concat", []byte("foo"), []byte("bar"))
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
if string(cat) != "foobar" {
|
|
t.Fatalf("want foobar, got %s", cat)
|
|
}
|
|
})
|
|
|
|
t.Run("bytes_流式输出", func(t *testing.T) {
|
|
bCh, err := Invoke[chan []byte](ctx, pool, "bytes_chunks", []byte("abcdefgh"), 3)
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
var got []string
|
|
for chunk := range bCh {
|
|
got = append(got, string(chunk))
|
|
}
|
|
want := []string{"abc", "def", "gh"}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("want %v, got %v", want, got)
|
|
}
|
|
})
|
|
}
|