mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-28 11:48:10 +08:00
### Summary Adopt sync.WaitGroup.Go (Go 1.25) to simplify tracked goroutine spawning. This replaces the error-prone trio of wg.Add(1), go func(), and defer wg.Done() with a single, self-contained call. More info: https://github.com/golang/go/issues/63796 Signed-off-by: grandpig <grandpig@outlook.com>
703 lines
22 KiB
Go
703 lines
22 KiB
Go
package graph
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"testing"
|
|
"time"
|
|
|
|
"ragflow/internal/harness/graph/channels"
|
|
"ragflow/internal/harness/graph/constants"
|
|
"ragflow/internal/harness/graph/errors"
|
|
"ragflow/internal/harness/graph/types"
|
|
)
|
|
|
|
// Channel type integration tests — all channel types executed through a compiled graph.
|
|
// These tests live in the graph package to avoid import cycle (graph imports channels).
|
|
|
|
// Helper: build a chain graph (Start→a→b→...→End).
|
|
type chainBuilder struct {
|
|
schema map[string]interface{}
|
|
chans map[string]channels.Channel
|
|
nodes []string
|
|
fns map[string]types.NodeFunc
|
|
}
|
|
|
|
func newChain(schema map[string]interface{}) *chainBuilder {
|
|
return &chainBuilder{
|
|
schema: schema,
|
|
chans: make(map[string]channels.Channel),
|
|
fns: make(map[string]types.NodeFunc),
|
|
}
|
|
}
|
|
|
|
func (b *chainBuilder) channel(name string, ch channels.Channel) *chainBuilder {
|
|
b.chans[name] = ch
|
|
return b
|
|
}
|
|
|
|
func (b *chainBuilder) node(name string, fn types.NodeFunc) *chainBuilder {
|
|
b.nodes = append(b.nodes, name)
|
|
b.fns[name] = fn
|
|
return b
|
|
}
|
|
|
|
func (b *chainBuilder) invoke(input interface{}) (interface{}, error) {
|
|
g := NewStateGraph(b.schema)
|
|
for name, ch := range b.chans {
|
|
g.AddChannel(name, ch)
|
|
}
|
|
for name, fn := range b.fns {
|
|
g.AddNode(name, fn)
|
|
}
|
|
if len(b.nodes) > 0 {
|
|
if err := g.AddEdge(constants.Start, b.nodes[0]); err != nil {
|
|
return nil, fmt.Errorf("AddEdge Start->%s: %w", b.nodes[0], err)
|
|
}
|
|
for i := 0; i < len(b.nodes)-1; i++ {
|
|
if err := g.AddEdge(b.nodes[i], b.nodes[i+1]); err != nil {
|
|
return nil, fmt.Errorf("AddEdge %s->%s: %w", b.nodes[i], b.nodes[i+1], err)
|
|
}
|
|
}
|
|
if err := g.AddEdge(b.nodes[len(b.nodes)-1], constants.End); err != nil {
|
|
return nil, fmt.Errorf("AddEdge %s->End: %w", b.nodes[len(b.nodes)-1], err)
|
|
}
|
|
}
|
|
cg, err := g.Compile()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Compile: %w", err)
|
|
}
|
|
return cg.Invoke(context.Background(), input)
|
|
}
|
|
|
|
// ============================================================
|
|
// BinaryOperatorAggregate tests
|
|
// ============================================================
|
|
|
|
func TestGraphChannel_BinaryOperator_AddAccumulation(t *testing.T) {
|
|
b := newChain(map[string]interface{}{"total": 0})
|
|
b.channel("total", channels.NewBinaryOperatorAggregate(int(0), channels.IntAdd))
|
|
b.node("a", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"total": 5}, nil
|
|
})
|
|
b.node("b", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"total": 10}, nil
|
|
})
|
|
|
|
result, err := b.invoke(map[string]interface{}{"total": 0})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
m := result.(map[string]interface{})
|
|
total, _ := m["total"].(int)
|
|
if total != 15 {
|
|
t.Errorf("expected total=15 (0+5+10), got %d", total)
|
|
}
|
|
}
|
|
|
|
func TestGraphChannel_BinaryOperator_Overwrite(t *testing.T) {
|
|
t.Skip("inline Pregel path does not support Overwrite wrapper; use pregel.Engine instead")
|
|
|
|
b := newChain(map[string]interface{}{"total": 0})
|
|
b.channel("total", channels.NewBinaryOperatorAggregate(int(0), channels.IntAdd))
|
|
b.node("writer", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"total": types.NewOverwrite(99)}, nil
|
|
})
|
|
|
|
result, err := b.invoke(map[string]interface{}{"total": 0})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
m := result.(map[string]interface{})
|
|
total, _ := m["total"].(int)
|
|
if total != 99 {
|
|
t.Errorf("expected total=99 (overwrite), got %d", total)
|
|
}
|
|
}
|
|
|
|
func TestGraphChannel_BinaryOperator_StringConcat(t *testing.T) {
|
|
b := newChain(map[string]interface{}{"text": ""})
|
|
b.channel("text", channels.NewBinaryOperatorAggregate("", channels.StringConcat))
|
|
b.node("a", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"text": "hello "}, nil
|
|
})
|
|
b.node("b", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"text": "world"}, nil
|
|
})
|
|
|
|
result, err := b.invoke(map[string]interface{}{"text": ""})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
m := result.(map[string]interface{})
|
|
text, _ := m["text"].(string)
|
|
if text != "hello world" {
|
|
t.Errorf("expected 'hello world', got %q", text)
|
|
}
|
|
}
|
|
|
|
func TestGraphChannel_BinaryOperator_ListAppend(t *testing.T) {
|
|
b := newChain(map[string]interface{}{"items": []interface{}{}})
|
|
b.channel("items", channels.NewBinaryOperatorAggregate([]interface{}{}, channels.ListAppend))
|
|
b.node("a", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"items": []interface{}{"a", "b"}}, nil
|
|
})
|
|
b.node("b", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"items": []interface{}{"c"}}, nil
|
|
})
|
|
|
|
result, err := b.invoke(map[string]interface{}{"items": []interface{}{}})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
m := result.(map[string]interface{})
|
|
items, _ := m["items"].([]interface{})
|
|
if len(items) != 3 {
|
|
t.Errorf("expected 3 items, got %d: %v", len(items), items)
|
|
}
|
|
}
|
|
|
|
func TestGraphChannel_BinaryOperator_MultipleSteps(t *testing.T) {
|
|
t.Skip("inline Pregel path recursion limit issue with conditional loops; use pregel.Engine instead")
|
|
|
|
g := NewStateGraph(map[string]interface{}{"total": 0})
|
|
g.AddChannel("total", channels.NewBinaryOperatorAggregate(int(0), channels.IntAdd))
|
|
g.AddNode("inc", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"total": 1}, nil
|
|
})
|
|
g.AddNode("done", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{}, nil
|
|
})
|
|
g.AddEdge(constants.Start, "inc")
|
|
g.AddConditionalEdges("inc",
|
|
func(_ context.Context, state interface{}) (interface{}, error) {
|
|
m := state.(map[string]interface{})
|
|
c, _ := m["total"].(int)
|
|
if c >= 5 {
|
|
return "done", nil
|
|
}
|
|
return "inc", nil
|
|
},
|
|
map[string]string{"inc": "inc", "done": "done"},
|
|
)
|
|
g.AddEdge("done", constants.End)
|
|
|
|
cg, err := g.Compile()
|
|
if err != nil {
|
|
t.Fatalf("Compile: %v", err)
|
|
}
|
|
result, err := cg.Invoke(context.Background(), map[string]interface{}{"total": 0})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
m := result.(map[string]interface{})
|
|
total, _ := m["total"].(int)
|
|
if total < 5 {
|
|
t.Errorf("expected total >= 5, got %d", total)
|
|
}
|
|
t.Logf("loop accumulated total: %d", total)
|
|
}
|
|
|
|
// ============================================================
|
|
// ReducerChannel tests
|
|
// ============================================================
|
|
|
|
func TestGraphChannel_Reducer_Basic(t *testing.T) {
|
|
inner := channels.NewLastValue(int(0))
|
|
rc := channels.NewReducerChannel(inner, func(current, update interface{}) interface{} {
|
|
ci, _ := current.(int)
|
|
ui, _ := update.(int)
|
|
return ci + ui
|
|
})
|
|
b := newChain(map[string]interface{}{"val": 0})
|
|
b.channel("val", rc)
|
|
b.node("a", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"val": 5}, nil
|
|
})
|
|
b.node("b", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"val": 10}, nil
|
|
})
|
|
|
|
result, err := b.invoke(map[string]interface{}{"val": 0})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
m := result.(map[string]interface{})
|
|
v, _ := m["val"].(int)
|
|
if v != 15 {
|
|
t.Errorf("expected val=15 (0+5+10), got %d", v)
|
|
}
|
|
}
|
|
|
|
func TestGraphChannel_Reducer_Append(t *testing.T) {
|
|
inner := channels.NewLastValue([]interface{}{})
|
|
rc := channels.NewReducerChannel(inner, channels.AppendReducer)
|
|
b := newChain(map[string]interface{}{"items": []interface{}{}})
|
|
b.channel("items", rc)
|
|
b.node("a", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"items": "a"}, nil
|
|
})
|
|
b.node("b", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"items": "b"}, nil
|
|
})
|
|
|
|
result, err := b.invoke(map[string]interface{}{"items": []interface{}{}})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
m := result.(map[string]interface{})
|
|
items, _ := m["items"].([]interface{})
|
|
if len(items) != 2 {
|
|
t.Errorf("expected 2 items, got %d: %v", len(items), items)
|
|
}
|
|
}
|
|
|
|
func TestGraphChannel_Reducer_Merge(t *testing.T) {
|
|
inner := channels.NewLastValue(map[string]interface{}{})
|
|
rc := channels.NewReducerChannel(inner, channels.MergeReducer)
|
|
b := newChain(map[string]interface{}{"data": map[string]interface{}{}})
|
|
b.channel("data", rc)
|
|
b.node("a", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"data": map[string]interface{}{"x": 1}}, nil
|
|
})
|
|
b.node("b", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"data": map[string]interface{}{"y": 2}}, nil
|
|
})
|
|
|
|
result, err := b.invoke(map[string]interface{}{"data": map[string]interface{}{}})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
m := result.(map[string]interface{})
|
|
data, _ := m["data"].(map[string]interface{})
|
|
if data["x"] != 1 || data["y"] != 2 {
|
|
t.Errorf("expected {x:1, y:2}, got %v", data)
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Topic tests
|
|
// ============================================================
|
|
|
|
func TestGraphChannel_Topic_Accumulate(t *testing.T) {
|
|
b := newChain(map[string]interface{}{"msgs": []interface{}{}})
|
|
b.channel("msgs", channels.NewTopic("", true))
|
|
b.node("a", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"msgs": "msg_a"}, nil
|
|
})
|
|
b.node("b", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"msgs": "msg_b"}, nil
|
|
})
|
|
|
|
result, err := b.invoke(map[string]interface{}{"msgs": []interface{}{}})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
m := result.(map[string]interface{})
|
|
msgs, _ := m["msgs"].([]interface{})
|
|
if len(msgs) != 2 {
|
|
t.Errorf("expected 2 messages, got %d: %v", len(msgs), msgs)
|
|
}
|
|
}
|
|
|
|
func TestGraphChannel_Topic_NoAccumulate(t *testing.T) {
|
|
b := newChain(map[string]interface{}{"msgs": []interface{}{}})
|
|
b.channel("msgs", channels.NewTopic("", false))
|
|
b.node("writer", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"msgs": "only"}, nil
|
|
})
|
|
|
|
result, err := b.invoke(map[string]interface{}{"msgs": []interface{}{}})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
m := result.(map[string]interface{})
|
|
msgs, _ := m["msgs"].([]interface{})
|
|
if len(msgs) != 1 {
|
|
t.Errorf("expected 1 message, got %d", len(msgs))
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// EphemeralValue test
|
|
// ============================================================
|
|
|
|
func TestGraphChannel_Ephemeral_InGraph(t *testing.T) {
|
|
b := newChain(map[string]interface{}{"temp": "", "persist": ""})
|
|
b.channel("temp", channels.NewEphemeralValue("", true))
|
|
b.channel("persist", channels.NewLastValue(""))
|
|
b.node("writer", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"temp": "ephemeral", "persist": "stored"}, nil
|
|
})
|
|
|
|
result, err := b.invoke(map[string]interface{}{"temp": "", "persist": ""})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
m := result.(map[string]interface{})
|
|
p, _ := m["persist"].(string)
|
|
if p != "stored" {
|
|
t.Errorf("expected stored, got %q", p)
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// AnyValue test
|
|
// ============================================================
|
|
|
|
func TestGraphChannel_AnyValue_InGraph(t *testing.T) {
|
|
b := newChain(map[string]interface{}{"val": ""})
|
|
b.channel("val", channels.NewAnyValue(""))
|
|
b.node("a", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"val": "from_a"}, nil
|
|
})
|
|
b.node("b", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"val": "from_b"}, nil
|
|
})
|
|
|
|
result, err := b.invoke(map[string]interface{}{"val": ""})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
m := result.(map[string]interface{})
|
|
v, _ := m["val"].(string)
|
|
if v != "from_b" {
|
|
t.Errorf("expected 'from_b' (last write wins), got %q", v)
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// UntrackedValue test
|
|
// ============================================================
|
|
|
|
func TestGraphChannel_Untracked_InGraph(t *testing.T) {
|
|
b := newChain(map[string]interface{}{"val": "", "secret": ""})
|
|
b.channel("val", channels.NewLastValue(""))
|
|
b.channel("secret", channels.NewUntrackedValue(""))
|
|
b.node("writer", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"val": "visible", "secret": "hidden"}, nil
|
|
})
|
|
|
|
result, err := b.invoke(map[string]interface{}{"val": "", "secret": ""})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
m := result.(map[string]interface{})
|
|
v, _ := m["val"].(string)
|
|
if v != "visible" {
|
|
t.Errorf("expected visible, got %q", v)
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Cross-channel interaction tests
|
|
// ============================================================
|
|
|
|
func TestGraphChannel_LastValueAndBinaryOp(t *testing.T) {
|
|
b := newChain(map[string]interface{}{"name": "", "total": 0})
|
|
b.channel("name", channels.NewLastValue(""))
|
|
b.channel("total", channels.NewBinaryOperatorAggregate(int(0), channels.IntAdd))
|
|
b.node("a", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"name": "alice", "total": 10}, nil
|
|
})
|
|
b.node("b", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"total": 20}, nil
|
|
})
|
|
|
|
result, err := b.invoke(map[string]interface{}{"name": "", "total": 0})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
m := result.(map[string]interface{})
|
|
name, _ := m["name"].(string)
|
|
total, _ := m["total"].(int)
|
|
if name != "alice" {
|
|
t.Errorf("expected alice, got %q", name)
|
|
}
|
|
if total != 30 {
|
|
t.Errorf("expected total=30 (0+10+20), got %d", total)
|
|
}
|
|
}
|
|
|
|
func TestGraphChannel_TopicAndBarrier(t *testing.T) {
|
|
b := newChain(map[string]interface{}{"msgs": []interface{}{}})
|
|
b.channel("msgs", channels.NewTopic("", true))
|
|
b.node("a", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"msgs": "from_a"}, nil
|
|
})
|
|
b.node("b", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"msgs": "from_b"}, nil
|
|
})
|
|
|
|
result, err := b.invoke(map[string]interface{}{"msgs": []interface{}{}})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
m := result.(map[string]interface{})
|
|
msgs, _ := m["msgs"].([]interface{})
|
|
if len(msgs) != 2 {
|
|
t.Errorf("expected 2 messages, got %d: %v", len(msgs), msgs)
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Race condition tests (channels are single-goroutine, so these
|
|
// verify the engine doesn't introduce races)
|
|
// ============================================================
|
|
|
|
func TestGraphChannel_Race_ConcurrentGraphInvocations(t *testing.T) {
|
|
g := NewStateGraph(map[string]interface{}{"val": ""})
|
|
g.AddChannel("val", channels.NewLastValue(""))
|
|
g.AddNode("echo", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"val": "done"}, nil
|
|
})
|
|
g.AddEdge(constants.Start, "echo")
|
|
g.AddEdge("echo", constants.End)
|
|
|
|
cg, err := g.Compile()
|
|
if err != nil {
|
|
t.Fatalf("Compile: %v", err)
|
|
}
|
|
|
|
const concurrency = 20
|
|
var wg sync.WaitGroup
|
|
errs := make(chan error, concurrency)
|
|
|
|
for i := 0; i < concurrency; i++ {
|
|
wg.Go(func() {
|
|
_, err := cg.Invoke(context.Background(), map[string]interface{}{"val": ""})
|
|
if err != nil {
|
|
errs <- err
|
|
}
|
|
})
|
|
}
|
|
wg.Wait()
|
|
close(errs)
|
|
|
|
for err := range errs {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Error propagation
|
|
// ============================================================
|
|
|
|
func TestGraphChannel_OverwriteConflict(t *testing.T) {
|
|
t.Skip("inline Pregel path does not support parallel fan-in; OverwriteConflict requires AllPredecessor mode")
|
|
}
|
|
|
|
func TestGraphChannel_BinaryOperator_EmptyUpdate(t *testing.T) {
|
|
b := newChain(map[string]interface{}{"total": 0})
|
|
b.channel("total", channels.NewBinaryOperatorAggregate(int(0), channels.IntAdd))
|
|
b.node("nop", func(_ context.Context, state interface{}) (interface{}, error) {
|
|
return map[string]interface{}{"total": nil}, nil
|
|
})
|
|
|
|
result, err := b.invoke(map[string]interface{}{"total": 42})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
m := result.(map[string]interface{})
|
|
total, _ := m["total"].(int)
|
|
if total != 42 {
|
|
t.Errorf("expected total=42 (unchanged), got %d", total)
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Timeout cancel
|
|
// ============================================================
|
|
|
|
func TestGraphChannel_TimeoutCancel(t *testing.T) {
|
|
g := NewStateGraph(map[string]interface{}{"val": ""})
|
|
g.AddChannel("val", channels.NewLastValue(""))
|
|
g.AddNode("slow", func(ctx context.Context, state interface{}) (interface{}, error) {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
case <-time.After(200 * time.Millisecond):
|
|
}
|
|
return map[string]interface{}{"val": "done"}, nil
|
|
})
|
|
g.AddEdge(constants.Start, "slow")
|
|
g.AddEdge("slow", constants.End)
|
|
|
|
cg, err := g.Compile()
|
|
if err != nil {
|
|
t.Fatalf("Compile: %v", err)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
|
|
defer cancel()
|
|
_, err = cg.Invoke(ctx, map[string]interface{}{"val": ""})
|
|
if err == nil {
|
|
t.Log("node completed before timeout")
|
|
} else {
|
|
t.Logf("timeout correctly triggered: %v", err)
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Checkpoint round-trip for channel types
|
|
// ============================================================
|
|
|
|
func TestGraphChannel_CheckpointRoundTrip(t *testing.T) {
|
|
type testCase struct {
|
|
name string
|
|
makeCh func() channels.Channel
|
|
setup func(channels.Channel)
|
|
verify func(channels.Channel, *testing.T)
|
|
}
|
|
|
|
cases := []testCase{
|
|
{
|
|
name: "LastValue",
|
|
makeCh: func() channels.Channel { ch := channels.NewLastValue(""); ch.SetKey("lv"); return ch },
|
|
setup: func(ch channels.Channel) { ch.Update([]interface{}{"stored"}) },
|
|
verify: func(ch channels.Channel, t *testing.T) {
|
|
v, _ := ch.Get()
|
|
if v != "stored" {
|
|
t.Errorf("expected 'stored', got %v", v)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "BinaryOperatorAggregate",
|
|
makeCh: func() channels.Channel {
|
|
ch := channels.NewBinaryOperatorAggregate(int(0), channels.IntAdd)
|
|
ch.SetKey("bo")
|
|
return ch
|
|
},
|
|
setup: func(ch channels.Channel) { ch.Update([]interface{}{7, 3}) },
|
|
verify: func(ch channels.Channel, t *testing.T) {
|
|
v, _ := ch.Get()
|
|
if v != 10 {
|
|
t.Errorf("expected 10, got %v", v)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "NamedBarrierValue",
|
|
makeCh: func() channels.Channel {
|
|
ch := channels.NewNamedBarrierValue(nil, []string{"x", "y"})
|
|
ch.SetKey("nb")
|
|
return ch
|
|
},
|
|
setup: func(ch channels.Channel) { ch.Update([]interface{}{"x", "y"}) },
|
|
verify: func(ch channels.Channel, t *testing.T) {
|
|
if !ch.IsAvailable() {
|
|
t.Error("barrier should be available")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "Topic",
|
|
makeCh: func() channels.Channel { ch := channels.NewTopic("", true); ch.SetKey("tp"); return ch },
|
|
setup: func(ch channels.Channel) { ch.Update([]interface{}{"a", "b"}) },
|
|
verify: func(ch channels.Channel, t *testing.T) {
|
|
v, _ := ch.Get()
|
|
items := v.([]interface{})
|
|
if len(items) != 2 {
|
|
t.Errorf("expected 2 items, got %d", len(items))
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "EphemeralValue",
|
|
makeCh: func() channels.Channel { ch := channels.NewEphemeralValue("", true); ch.SetKey("ev"); return ch },
|
|
setup: func(ch channels.Channel) { ch.Update([]interface{}{"temp"}) },
|
|
verify: func(ch channels.Channel, t *testing.T) {
|
|
v, _ := ch.Get()
|
|
if v != "temp" {
|
|
t.Errorf("expected 'temp', got %v", v)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "AnyValue",
|
|
makeCh: func() channels.Channel { ch := channels.NewAnyValue(""); ch.SetKey("av"); return ch },
|
|
setup: func(ch channels.Channel) { ch.Update([]interface{}{"stored"}) },
|
|
verify: func(ch channels.Channel, t *testing.T) {
|
|
v, _ := ch.Get()
|
|
if v != "stored" {
|
|
t.Errorf("expected 'stored', got %v", v)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "ReducerChannel",
|
|
makeCh: func() channels.Channel {
|
|
inner := channels.NewLastValue(int(0))
|
|
ch := channels.NewReducerChannel(inner, channels.AddReducer)
|
|
ch.SetKey("rc")
|
|
return ch
|
|
},
|
|
setup: func(ch channels.Channel) { ch.Update([]interface{}{5, 3}) },
|
|
verify: func(ch channels.Channel, t *testing.T) {
|
|
v, _ := ch.Get()
|
|
if v != 8 {
|
|
t.Errorf("expected 8, got %v", v)
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
orig := tc.makeCh()
|
|
tc.setup(orig)
|
|
cp := orig.Checkpoint()
|
|
restored := orig.FromCheckpoint(cp)
|
|
tc.verify(restored, t)
|
|
})
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// BinaryOperator edge cases
|
|
// ============================================================
|
|
|
|
func TestGraphChannel_BinaryOperator_IntAddEdgeCases(t *testing.T) {
|
|
if v := channels.IntAdd(1.5, 2.5); v != 4.0 {
|
|
t.Errorf("expected 4.0, got %v", v)
|
|
}
|
|
if v := channels.IntAdd(5, "not a number"); v != 5 {
|
|
t.Errorf("expected 5 (unchanged on type mismatch), got %v", v)
|
|
}
|
|
}
|
|
|
|
func TestGraphChannel_BinaryOperator_ListAppendEdgeCases(t *testing.T) {
|
|
r := channels.ListAppend([]interface{}{"a"}, "b")
|
|
items := r.([]interface{})
|
|
if len(items) != 2 || items[1] != "b" {
|
|
t.Errorf("expected [a, b], got %v", items)
|
|
}
|
|
r2 := channels.ListAppend("a", "b")
|
|
items2 := r2.([]interface{})
|
|
if len(items2) != 2 {
|
|
t.Errorf("expected 2 items, got %d", len(items2))
|
|
}
|
|
}
|
|
|
|
func TestGraphChannel_StringConcatEdgeCases(t *testing.T) {
|
|
r := channels.StringConcat(1, 2)
|
|
if r != "12" {
|
|
t.Errorf("expected '12', got %q", r)
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Errors package verification
|
|
// ============================================================
|
|
|
|
func TestGraphChannel_Errors(t *testing.T) {
|
|
if !errors.IsEmptyChannelError(&errors.EmptyChannelError{}) {
|
|
t.Error("IsEmptyChannelError should detect EmptyChannelError")
|
|
}
|
|
if !errors.IsInvalidUpdateError(&errors.InvalidUpdateError{Message: "test"}) {
|
|
t.Error("IsInvalidUpdateError should detect InvalidUpdateError")
|
|
}
|
|
}
|