325 lines
7.0 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|