Files
2026-07-13 12:49:10 +08:00

325 lines
7.0 KiB
Go

/*
* Copyright 2026 CloudWeGo Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package schema_test
import (
"errors"
"io"
"testing"
"time"
"github.com/cloudwego/eino/schema"
)
func recvAll(t *testing.T, sr *schema.StreamReader[string]) ([]string, []error) {
t.Helper()
var vals []string
var errs []error
for {
v, err := sr.Recv()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
errs = append(errs, err)
continue
}
vals = append(vals, v)
}
return vals, errs
}
func makeStream(items []string, opts ...schema.ConvertOption) *schema.StreamReader[string] {
return schema.StreamReaderWithConvert(
schema.StreamReaderFromArray(items),
func(s string) (string, error) { return s, nil },
opts...,
)
}
func TestWithOnEOF_PassThroughEOF(t *testing.T) {
items := []string{"a", "b", "c", "d"}
sr := makeStream(items, schema.WithOnEOF(func() (any, error) {
return nil, io.EOF
}))
defer sr.Close()
vals, errs := recvAll(t, sr)
if len(errs) != 0 {
t.Fatalf("expected no errors, got %v", errs)
}
if len(vals) != 4 {
t.Fatalf("expected 4 values, got %d: %v", len(vals), vals)
}
for i, want := range items {
if vals[i] != want {
t.Errorf("vals[%d] = %q, want %q", i, vals[i], want)
}
}
}
func TestWithOnEOF_InjectError(t *testing.T) {
items := []string{"a", "b", "c", "d"}
customErr := errors.New("validation failed")
sr := makeStream(items, schema.WithOnEOF(func() (any, error) {
return nil, customErr
}))
defer sr.Close()
var vals []string
var gotCustomErr bool
for {
v, err := sr.Recv()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
if errors.Is(err, customErr) {
gotCustomErr = true
continue
}
t.Fatalf("unexpected error: %v", err)
}
vals = append(vals, v)
}
if len(vals) != 4 {
t.Fatalf("expected 4 values, got %d: %v", len(vals), vals)
}
if !gotCustomErr {
t.Fatalf("expected custom error from onEOF, did not receive it")
}
}
func TestWithOnEOF_InjectValue(t *testing.T) {
items := []string{"a", "b", "c", "d"}
sr := makeStream(items, schema.WithOnEOF(func() (any, error) {
return "extra", nil
}))
defer sr.Close()
var vals []string
for {
v, err := sr.Recv()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
vals = append(vals, v)
}
if len(vals) != 5 {
t.Fatalf("expected 5 values, got %d: %v", len(vals), vals)
}
if vals[4] != "extra" {
t.Errorf("vals[4] = %q, want %q", vals[4], "extra")
}
}
func TestWithOnEOF_BlockingCallback(t *testing.T) {
sr, sw := schema.Pipe[string](0)
unblock := make(chan struct{})
converted := schema.StreamReaderWithConvert(sr,
func(s string) (string, error) { return s, nil },
schema.WithOnEOF(func() (any, error) {
<-unblock
return "after-block", nil
}),
)
defer converted.Close()
go func() {
sw.Send("x", nil)
sw.Close()
}()
v, err := converted.Recv()
if err != nil {
t.Fatalf("first Recv error: %v", err)
}
if v != "x" {
t.Fatalf("first Recv = %q, want %q", v, "x")
}
done := make(chan struct{})
var recvVal string
var recvErr error
go func() {
recvVal, recvErr = converted.Recv()
close(done)
}()
select {
case <-done:
t.Fatal("Recv returned before unblock signal")
case <-time.After(50 * time.Millisecond):
}
close(unblock)
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("Recv did not return after unblock signal")
}
if recvErr != nil {
t.Fatalf("second Recv error: %v", recvErr)
}
if recvVal != "after-block" {
t.Errorf("second Recv = %q, want %q", recvVal, "after-block")
}
v3, err3 := converted.Recv()
if !errors.Is(err3, io.EOF) {
t.Fatalf("third Recv: got (%q, %v), want EOF", v3, err3)
}
}
func TestWithOnEOF_EmptyStream(t *testing.T) {
customErr := errors.New("empty stream error")
sr := makeStream(nil, schema.WithOnEOF(func() (any, error) {
return nil, customErr
}))
defer sr.Close()
v, err := sr.Recv()
if !errors.Is(err, customErr) {
t.Fatalf("first Recv: got (%q, %v), want customErr", v, err)
}
v2, err2 := sr.Recv()
if !errors.Is(err2, io.EOF) {
t.Fatalf("second Recv: got (%q, %v), want EOF", v2, err2)
}
}
func TestWithOnEOF_WithErrWrapper_ErrorPath(t *testing.T) {
sr, sw := schema.Pipe[string](0)
streamErr := errors.New("stream error")
onEOFCalled := false
converted := schema.StreamReaderWithConvert(sr,
func(s string) (string, error) { return s, nil },
schema.WithErrWrapper(func(err error) error {
return err
}),
schema.WithOnEOF(func() (any, error) {
onEOFCalled = true
return nil, errors.New("should not happen")
}),
)
defer converted.Close()
go func() {
sw.Send("a", nil)
sw.Send("", streamErr)
sw.Close()
}()
v, err := converted.Recv()
if err != nil {
t.Fatalf("first Recv error: %v", err)
}
if v != "a" {
t.Fatalf("first Recv = %q, want %q", v, "a")
}
_, err = converted.Recv()
if !errors.Is(err, streamErr) {
t.Fatalf("second Recv: got %v, want streamErr", err)
}
if onEOFCalled {
t.Fatal("onEOF should not have been called when stream errored")
}
}
func TestWithOnEOF_WithErrWrapper_EOFPath(t *testing.T) {
items := []string{"a", "b", "c"}
errWrapperCalled := false
sr := schema.StreamReaderWithConvert(
schema.StreamReaderFromArray(items),
func(s string) (string, error) { return s, nil },
schema.WithErrWrapper(func(err error) error {
errWrapperCalled = true
return err
}),
schema.WithOnEOF(func() (any, error) {
return "oneof-val", nil
}),
)
defer sr.Close()
var vals []string
for {
v, err := sr.Recv()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
vals = append(vals, v)
}
if len(vals) != 4 {
t.Fatalf("expected 4 values, got %d: %v", len(vals), vals)
}
if vals[3] != "oneof-val" {
t.Errorf("vals[3] = %q, want %q", vals[3], "oneof-val")
}
if errWrapperCalled {
t.Fatal("errWrapper should not have been called for clean stream")
}
}
func TestWithOnEOF_MultipleRecvAfterEOF(t *testing.T) {
items := []string{"a"}
customErr := errors.New("oneof error")
sr := makeStream(items, schema.WithOnEOF(func() (any, error) {
return nil, customErr
}))
defer sr.Close()
v, err := sr.Recv()
if err != nil {
t.Fatalf("first Recv error: %v", err)
}
if v != "a" {
t.Fatalf("first Recv = %q, want %q", v, "a")
}
_, err = sr.Recv()
if !errors.Is(err, customErr) {
t.Fatalf("second Recv: got %v, want customErr", err)
}
for i := 0; i < 5; i++ {
_, err = sr.Recv()
if !errors.Is(err, io.EOF) {
t.Fatalf("Recv #%d after onEOF: got %v, want io.EOF", i+3, err)
}
}
}