mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-12 22:55:45 +08:00
### What problem does this PR solve? - OpenTelemetry integration - Checkpoint conformance tests - State inspector API - Callbacks - A series of fault injection tests - Pregel integration tests ### Type of change - [x] Refactoring
679 lines
18 KiB
Go
679 lines
18 KiB
Go
// Package checkpoint conformance tests verify that all checkpointer implementations
|
|
// satisfy the BaseCheckpointer contract.
|
|
//
|
|
// This mirrors Python's langgraph-checkpoint-conformance package.
|
|
// Any type implementing checkpoint.BaseCheckpointer should pass this suite.
|
|
package checkpoint
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
"ragflow/internal/harness/graph/constants"
|
|
)
|
|
|
|
// ConformanceTestSuite holds state shared across conformance tests.
|
|
// checkpointer under test: factory function returning a fresh instance.
|
|
type ConformanceTestSuite struct {
|
|
// NewCheckpointer creates a fresh checkpointer instance for each sub-test.
|
|
NewCheckpointer func() BaseCheckpointer
|
|
}
|
|
|
|
// RunAll runs all conformance tests against the given checkpointer factory.
|
|
func (suite *ConformanceTestSuite) RunAll(t *testing.T) {
|
|
t.Helper()
|
|
t.Run("PutAndGet", suite.TestPutAndGet)
|
|
t.Run("PutAndGetByID", suite.TestPutAndGetByID)
|
|
t.Run("ListEmpty", suite.TestListEmpty)
|
|
t.Run("ListOrder", suite.TestListOrder)
|
|
t.Run("ListWithLimit", suite.TestListWithLimit)
|
|
t.Run("MultipleThreads", suite.TestMultipleThreads)
|
|
t.Run("GetNonExistent", suite.TestGetNonExistent)
|
|
t.Run("OverwriteExisting", suite.TestOverwriteExisting)
|
|
t.Run("ListAcrossThreads", suite.TestListAcrossThreads)
|
|
t.Run("PutPreservesData", suite.TestPutPreservesData)
|
|
t.Run("DeepCopySemantics", suite.TestDeepCopySemantics)
|
|
t.Run("ConcurrentAccess", suite.TestConcurrentAccess)
|
|
t.Run("ManyCheckpoints", suite.TestManyCheckpoints)
|
|
t.Run("EmptyValues", suite.TestEmptyValues)
|
|
t.Run("NilConfig", suite.TestNilConfig)
|
|
}
|
|
|
|
// threadConfig creates a minimal checkpointer config from a thread ID.
|
|
func threadConfig(tid string) map[string]interface{} {
|
|
return map[string]interface{}{
|
|
constants.ConfigKeyThreadID: tid,
|
|
}
|
|
}
|
|
|
|
// threadConfigWithID creates a config with both thread and checkpoint ID.
|
|
func threadConfigWithID(tid, cpid string) map[string]interface{} {
|
|
return map[string]interface{}{
|
|
constants.ConfigKeyThreadID: tid,
|
|
constants.ConfigKeyCheckpointID: cpid,
|
|
}
|
|
}
|
|
|
|
// ---- Test cases ----
|
|
|
|
// TestPutAndGet verifies basic write-then-read.
|
|
func (suite *ConformanceTestSuite) TestPutAndGet(t *testing.T) {
|
|
cp := suite.NewCheckpointer()
|
|
ctx := context.Background()
|
|
|
|
tid := "test-thread-putget"
|
|
data := map[string]interface{}{"key1": "value1", "key2": 42, "key3": true}
|
|
|
|
if err := cp.Put(ctx, threadConfig(tid), data); err != nil {
|
|
t.Fatalf("Put failed: %v", err)
|
|
}
|
|
|
|
got, err := cp.Get(ctx, threadConfig(tid))
|
|
if err != nil {
|
|
t.Fatalf("Get failed: %v", err)
|
|
}
|
|
if got == nil {
|
|
t.Fatal("Get returned nil, expected checkpoint data")
|
|
}
|
|
assertMapEqual(t, data, got, "Put/Get round-trip")
|
|
}
|
|
|
|
// TestPutAndGetByID verifies getting a specific checkpoint by ID.
|
|
func (suite *ConformanceTestSuite) TestPutAndGetByID(t *testing.T) {
|
|
cp := suite.NewCheckpointer()
|
|
ctx := context.Background()
|
|
|
|
tid := "test-thread-byid"
|
|
data1 := map[string]interface{}{"version": 1}
|
|
data2 := map[string]interface{}{"version": 2}
|
|
|
|
if err := cp.Put(ctx, threadConfig(tid), data1); err != nil {
|
|
t.Fatalf("first Put failed: %v", err)
|
|
}
|
|
|
|
// Get the ID of the first checkpoint from List.
|
|
entries, err := cp.List(ctx, threadConfig(tid), 10)
|
|
if err != nil {
|
|
t.Fatalf("List failed: %v", err)
|
|
}
|
|
if len(entries) == 0 {
|
|
t.Fatal("List returned 0 entries after first Put")
|
|
}
|
|
cpID1 := entries[0][constants.ConfigKeyCheckpointID].(string)
|
|
|
|
if err := cp.Put(ctx, threadConfig(tid), data2); err != nil {
|
|
t.Fatalf("second Put failed: %v", err)
|
|
}
|
|
|
|
// Get by first checkpoint ID — must return data1.
|
|
got, err := cp.Get(ctx, threadConfigWithID(tid, cpID1))
|
|
if err != nil {
|
|
t.Fatalf("Get by ID failed: %v", err)
|
|
}
|
|
if got == nil {
|
|
t.Fatal("Get by ID returned nil")
|
|
}
|
|
v, ok := got["version"]
|
|
if !ok {
|
|
t.Fatalf("expected version=1, got %v", got)
|
|
}
|
|
// JSON may convert ints to float64.
|
|
var versionVal int
|
|
switch vt := v.(type) {
|
|
case int:
|
|
versionVal = vt
|
|
case float64:
|
|
versionVal = int(vt)
|
|
default:
|
|
t.Fatalf("unexpected type for version: %T", v)
|
|
}
|
|
if versionVal != 1 {
|
|
t.Fatalf("expected version=1, got %d (raw=%v)", versionVal, v)
|
|
}
|
|
}
|
|
|
|
// TestListEmpty verifies List returns nil/empty for a thread with no checkpoints.
|
|
func (suite *ConformanceTestSuite) TestListEmpty(t *testing.T) {
|
|
cp := suite.NewCheckpointer()
|
|
ctx := context.Background()
|
|
|
|
entries, err := cp.List(ctx, threadConfig("nonexistent-thread"), 10)
|
|
if err != nil {
|
|
t.Fatalf("List on empty thread failed: %v", err)
|
|
}
|
|
if len(entries) != 0 {
|
|
t.Fatalf("expected 0 entries for empty thread, got %d", len(entries))
|
|
}
|
|
}
|
|
|
|
// TestListOrder verifies List returns checkpoints in reverse chronological order.
|
|
func (suite *ConformanceTestSuite) TestListOrder(t *testing.T) {
|
|
cp := suite.NewCheckpointer()
|
|
ctx := context.Background()
|
|
|
|
tid := "test-thread-order"
|
|
n := 5
|
|
for i := 0; i < n; i++ {
|
|
data := map[string]interface{}{"i": i}
|
|
if err := cp.Put(ctx, threadConfig(tid), data); err != nil {
|
|
t.Fatalf("Put #%d failed: %v", i, err)
|
|
}
|
|
time.Sleep(time.Millisecond) // ensure timestamp ordering
|
|
}
|
|
|
|
entries, err := cp.List(ctx, threadConfig(tid), n)
|
|
if err != nil {
|
|
t.Fatalf("List failed: %v", err)
|
|
}
|
|
if len(entries) != n {
|
|
t.Fatalf("expected %d entries, got %d", n, len(entries))
|
|
}
|
|
|
|
// Verify reverse chronological order.
|
|
lastTime := time.Now().Add(time.Hour)
|
|
seenIDs := make(map[string]bool)
|
|
for i, entry := range entries {
|
|
if entry[constants.ConfigKeyCheckpointID] == nil {
|
|
t.Fatalf("entry %d missing checkpoint_id", i)
|
|
}
|
|
cpID, ok := entry[constants.ConfigKeyCheckpointID].(string)
|
|
if !ok || cpID == "" {
|
|
t.Fatalf("entry %d has invalid checkpoint_id: %v", i, entry[constants.ConfigKeyCheckpointID])
|
|
}
|
|
if seenIDs[cpID] {
|
|
t.Fatalf("duplicate checkpoint ID: %s", cpID)
|
|
}
|
|
seenIDs[cpID] = true
|
|
|
|
createdAt, ok := entry["created_at"].(time.Time)
|
|
if ok {
|
|
if createdAt.After(lastTime) {
|
|
t.Fatalf("entry %d: created_at %v is after previous %v (not reverse chronological)", i, createdAt, lastTime)
|
|
}
|
|
lastTime = createdAt
|
|
}
|
|
if entry["thread_id"] == nil {
|
|
val := entry[constants.ConfigKeyThreadID]
|
|
if val == nil {
|
|
t.Fatalf("entry %d missing thread_id", i)
|
|
}
|
|
}
|
|
if entry["parent_id"] == nil && i < n-1 {
|
|
// Parent chain: later entries have earlier parent_ids.
|
|
// Entry n-1 (oldest) may not have parent_id.
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestListWithLimit verifies List respects the limit parameter.
|
|
func (suite *ConformanceTestSuite) TestListWithLimit(t *testing.T) {
|
|
cp := suite.NewCheckpointer()
|
|
ctx := context.Background()
|
|
|
|
tid := "test-thread-limit"
|
|
for i := 0; i < 10; i++ {
|
|
data := map[string]interface{}{"i": i}
|
|
if err := cp.Put(ctx, threadConfig(tid), data); err != nil {
|
|
t.Fatalf("Put #%d failed: %v", i, err)
|
|
}
|
|
}
|
|
|
|
entries, err := cp.List(ctx, threadConfig(tid), 3)
|
|
if err != nil {
|
|
t.Fatalf("List with limit failed: %v", err)
|
|
}
|
|
if len(entries) != 3 {
|
|
t.Fatalf("expected 3 entries with limit=3, got %d", len(entries))
|
|
}
|
|
}
|
|
|
|
// TestMultipleThreads verifies isolation between threads.
|
|
func (suite *ConformanceTestSuite) TestMultipleThreads(t *testing.T) {
|
|
cp := suite.NewCheckpointer()
|
|
ctx := context.Background()
|
|
|
|
threads := []string{"thread-a", "thread-b", "thread-c"}
|
|
for _, tid := range threads {
|
|
data := map[string]interface{}{"owner": tid}
|
|
if err := cp.Put(ctx, threadConfig(tid), data); err != nil {
|
|
t.Fatalf("Put for %s failed: %v", tid, err)
|
|
}
|
|
}
|
|
|
|
for _, tid := range threads {
|
|
got, err := cp.Get(ctx, threadConfig(tid))
|
|
if err != nil {
|
|
t.Fatalf("Get for %s failed: %v", tid, err)
|
|
}
|
|
if got == nil {
|
|
t.Fatalf("Get for %s returned nil", tid)
|
|
}
|
|
owner, ok := got["owner"].(string)
|
|
if !ok || owner != tid {
|
|
t.Fatalf("expected owner=%q, got %q (data=%v)", tid, owner, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestGetNonExistent verifies Get returns nil for non-existent threads.
|
|
func (suite *ConformanceTestSuite) TestGetNonExistent(t *testing.T) {
|
|
cp := suite.NewCheckpointer()
|
|
ctx := context.Background()
|
|
|
|
got, err := cp.Get(ctx, threadConfig("does-not-exist"))
|
|
if err != nil {
|
|
t.Fatalf("Get on non-existent thread failed: %v", err)
|
|
}
|
|
if got != nil {
|
|
t.Fatalf("expected nil for non-existent thread, got %v", got)
|
|
}
|
|
}
|
|
|
|
// TestOverwriteExisting verifies Put with same thread ID replaces latest.
|
|
func (suite *ConformanceTestSuite) TestOverwriteExisting(t *testing.T) {
|
|
cp := suite.NewCheckpointer()
|
|
ctx := context.Background()
|
|
|
|
tid := "test-thread-overwrite"
|
|
v1 := map[string]interface{}{"value": "first"}
|
|
if err := cp.Put(ctx, threadConfig(tid), v1); err != nil {
|
|
t.Fatalf("first Put failed: %v", err)
|
|
}
|
|
|
|
v2 := map[string]interface{}{"value": "second"}
|
|
if err := cp.Put(ctx, threadConfig(tid), v2); err != nil {
|
|
t.Fatalf("second Put failed: %v", err)
|
|
}
|
|
|
|
got, err := cp.Get(ctx, threadConfig(tid))
|
|
if err != nil {
|
|
t.Fatalf("Get failed: %v", err)
|
|
}
|
|
if got == nil {
|
|
t.Fatal("Get returned nil")
|
|
}
|
|
if v, ok := got["value"].(string); !ok || v != "second" {
|
|
t.Fatalf("expected value=second, got %v", got)
|
|
}
|
|
}
|
|
|
|
// TestListAcrossThreads verifies List only returns entries for the specified thread.
|
|
func (suite *ConformanceTestSuite) TestListAcrossThreads(t *testing.T) {
|
|
cp := suite.NewCheckpointer()
|
|
ctx := context.Background()
|
|
|
|
for i := 0; i < 3; i++ {
|
|
tid := fmt.Sprintf("thread-%d", i)
|
|
if err := cp.Put(ctx, threadConfig(tid), map[string]interface{}{"i": i}); err != nil {
|
|
t.Fatalf("Put for %s failed: %v", tid, err)
|
|
}
|
|
}
|
|
|
|
for i := 0; i < 3; i++ {
|
|
tid := fmt.Sprintf("thread-%d", i)
|
|
entries, err := cp.List(ctx, threadConfig(tid), 10)
|
|
if err != nil {
|
|
t.Fatalf("List for %s failed: %v", tid, err)
|
|
}
|
|
if len(entries) != 1 {
|
|
t.Fatalf("expected 1 entry for %s, got %d", tid, len(entries))
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestPutPreservesData verifies all types of data are preserved.
|
|
func (suite *ConformanceTestSuite) TestPutPreservesData(t *testing.T) {
|
|
cp := suite.NewCheckpointer()
|
|
ctx := context.Background()
|
|
|
|
tid := "test-thread-types"
|
|
data := map[string]interface{}{
|
|
"string": "hello",
|
|
"int": 42,
|
|
"float": 3.14,
|
|
"bool": true,
|
|
"list": []interface{}{1, "two", 3.0},
|
|
"map": map[string]interface{}{"nested": "value", "num": 1},
|
|
"nil_val": nil,
|
|
"empty_str": "",
|
|
}
|
|
|
|
if err := cp.Put(ctx, threadConfig(tid), data); err != nil {
|
|
t.Fatalf("Put failed: %v", err)
|
|
}
|
|
|
|
got, err := cp.Get(ctx, threadConfig(tid))
|
|
if err != nil {
|
|
t.Fatalf("Get failed: %v", err)
|
|
}
|
|
if got == nil {
|
|
t.Fatal("Get returned nil")
|
|
}
|
|
|
|
assertMapEqual(t, data, got, "data preservation")
|
|
}
|
|
|
|
// TestDeepCopySemantics verifies that Put stores a copy, not a reference.
|
|
func (suite *ConformanceTestSuite) TestDeepCopySemantics(t *testing.T) {
|
|
cp := suite.NewCheckpointer()
|
|
ctx := context.Background()
|
|
|
|
tid := "test-thread-deepcopy"
|
|
data := map[string]interface{}{
|
|
"key": "original",
|
|
}
|
|
if err := cp.Put(ctx, threadConfig(tid), data); err != nil {
|
|
t.Fatalf("Put failed: %v", err)
|
|
}
|
|
|
|
// Modify the original map after Put.
|
|
data["key"] = "modified"
|
|
|
|
got, err := cp.Get(ctx, threadConfig(tid))
|
|
if err != nil {
|
|
t.Fatalf("Get failed: %v", err)
|
|
}
|
|
if got == nil {
|
|
t.Fatal("Get returned nil")
|
|
}
|
|
if v := got["key"]; v != "original" {
|
|
t.Fatalf("expected copy semantics: key=original, got %v", v)
|
|
}
|
|
}
|
|
|
|
// TestConcurrentAccess verifies thread safety under concurrent Put/Get operations.
|
|
func (suite *ConformanceTestSuite) TestConcurrentAccess(t *testing.T) {
|
|
cp := suite.NewCheckpointer()
|
|
ctx := context.Background()
|
|
|
|
const goroutines = 20
|
|
const opsPerGoroutine = 50
|
|
|
|
errCh := make(chan error, goroutines)
|
|
for g := 0; g < goroutines; g++ {
|
|
go func(gid int) {
|
|
tid := fmt.Sprintf("concurrent-thread-%d", gid)
|
|
for i := 0; i < opsPerGoroutine; i++ {
|
|
data := map[string]interface{}{"gid": gid, "i": i}
|
|
if err := cp.Put(ctx, threadConfig(tid), data); err != nil {
|
|
errCh <- fmt.Errorf("goroutine %d put failed: %w", gid, err)
|
|
return
|
|
}
|
|
got, err := cp.Get(ctx, threadConfig(tid))
|
|
if err != nil {
|
|
errCh <- fmt.Errorf("goroutine %d get failed: %w", gid, err)
|
|
return
|
|
}
|
|
if got == nil {
|
|
errCh <- fmt.Errorf("goroutine %d got nil after put", gid)
|
|
return
|
|
}
|
|
}
|
|
errCh <- nil
|
|
}(g)
|
|
}
|
|
|
|
for g := 0; g < goroutines; g++ {
|
|
if err := <-errCh; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestManyCheckpoints verifies performance with many checkpoints.
|
|
func (suite *ConformanceTestSuite) TestManyCheckpoints(t *testing.T) {
|
|
cp := suite.NewCheckpointer()
|
|
ctx := context.Background()
|
|
|
|
const n = 100
|
|
tid := "test-thread-many"
|
|
|
|
for i := 0; i < n; i++ {
|
|
data := map[string]interface{}{"index": i, "data": fmt.Sprintf("checkpoint-%d", i)}
|
|
if err := cp.Put(ctx, threadConfig(tid), data); err != nil {
|
|
t.Fatalf("Put #%d failed: %v", i, err)
|
|
}
|
|
}
|
|
|
|
entries, err := cp.List(ctx, threadConfig(tid), n)
|
|
if err != nil {
|
|
t.Fatalf("List failed: %v", err)
|
|
}
|
|
if len(entries) != n {
|
|
t.Fatalf("expected %d entries, got %d", n, len(entries))
|
|
}
|
|
|
|
got, err := cp.Get(ctx, threadConfig(tid))
|
|
if err != nil {
|
|
t.Fatalf("Get failed: %v", err)
|
|
}
|
|
if got == nil {
|
|
t.Fatal("Get returned nil")
|
|
}
|
|
idxRaw, ok := got["index"]
|
|
if !ok {
|
|
t.Fatalf("expected index key, got %v", got)
|
|
}
|
|
var idxVal int
|
|
switch vt := idxRaw.(type) {
|
|
case int:
|
|
idxVal = vt
|
|
case float64:
|
|
idxVal = int(vt)
|
|
default:
|
|
t.Fatalf("unexpected type for index: %T", idxRaw)
|
|
}
|
|
if idxVal != n-1 {
|
|
t.Fatalf("expected latest index=%d, got %d (raw=%v)", n-1, idxVal, idxRaw)
|
|
}
|
|
}
|
|
|
|
// TestEmptyValues verifies round-trip with empty maps.
|
|
func (suite *ConformanceTestSuite) TestEmptyValues(t *testing.T) {
|
|
cp := suite.NewCheckpointer()
|
|
ctx := context.Background()
|
|
|
|
tid := "test-thread-empty"
|
|
empty := map[string]interface{}{}
|
|
if err := cp.Put(ctx, threadConfig(tid), empty); err != nil {
|
|
t.Fatalf("Put with empty map failed: %v", err)
|
|
}
|
|
|
|
got, err := cp.Get(ctx, threadConfig(tid))
|
|
if err != nil {
|
|
t.Fatalf("Get after empty Put failed: %v", err)
|
|
}
|
|
if got == nil {
|
|
t.Fatal("Get returned nil after Put with empty map")
|
|
}
|
|
}
|
|
|
|
// TestNilConfig verifies Get/Put with missing thread_id returns an error.
|
|
func (suite *ConformanceTestSuite) TestNilConfig(t *testing.T) {
|
|
cp := suite.NewCheckpointer()
|
|
ctx := context.Background()
|
|
|
|
// Put without thread_id should fail.
|
|
err := cp.Put(ctx, map[string]interface{}{}, map[string]interface{}{"key": "val"})
|
|
if err == nil {
|
|
t.Fatal("expected error for Put without thread_id, got nil")
|
|
}
|
|
|
|
// Get without thread_id should fail.
|
|
_, err = cp.Get(ctx, map[string]interface{}{})
|
|
if err == nil {
|
|
t.Fatal("expected error for Get without thread_id, got nil")
|
|
}
|
|
|
|
// List without thread_id should fail.
|
|
_, err = cp.List(ctx, map[string]interface{}{}, 10)
|
|
if err == nil {
|
|
t.Fatal("expected error for List without thread_id, got nil")
|
|
}
|
|
}
|
|
|
|
// ---- Helpers ----
|
|
|
|
// assertMapEqual compares two maps and reports differences.
|
|
func assertMapEqual(t *testing.T, expected, actual map[string]interface{}, context string) {
|
|
t.Helper()
|
|
|
|
if len(expected) != len(actual) {
|
|
t.Fatalf("%s: map size mismatch: expected %d keys, got %d\n expected=%v\n actual=%v",
|
|
context, len(expected), len(actual), keysOf(expected), keysOf(actual))
|
|
}
|
|
|
|
for k, expectedVal := range expected {
|
|
actualVal, ok := actual[k]
|
|
if !ok {
|
|
t.Fatalf("%s: expected key %q not found in actual map", context, k)
|
|
}
|
|
if !valuesEqual(expectedVal, actualVal) {
|
|
t.Fatalf("%s: key %q: expected %v (type=%T), got %v (type=%T)",
|
|
context, k, expectedVal, expectedVal, actualVal, actualVal)
|
|
}
|
|
}
|
|
}
|
|
|
|
// keysOf returns sorted keys of a map.
|
|
func keysOf(m map[string]interface{}) []string {
|
|
keys := make([]string, 0, len(m))
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
// valuesEqual does a deep comparison of two values.
|
|
// Numeric types (int/float64) are compared by value to handle JSON
|
|
// serialization where ints become float64.
|
|
func valuesEqual(a, b interface{}) bool {
|
|
if a == nil && b == nil {
|
|
return true
|
|
}
|
|
if a == nil || b == nil {
|
|
return false
|
|
}
|
|
switch va := a.(type) {
|
|
case map[string]interface{}:
|
|
vb, ok := b.(map[string]interface{})
|
|
if !ok {
|
|
return false
|
|
}
|
|
if len(va) != len(vb) {
|
|
return false
|
|
}
|
|
for k, av := range va {
|
|
bv, ok := vb[k]
|
|
if !ok {
|
|
return false
|
|
}
|
|
if !valuesEqual(av, bv) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
case []interface{}:
|
|
vb, ok := b.([]interface{})
|
|
if !ok || len(va) != len(vb) {
|
|
return false
|
|
}
|
|
for i := range va {
|
|
if !valuesEqual(va[i], vb[i]) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
case string:
|
|
vb, ok := b.(string)
|
|
return ok && va == vb
|
|
case int:
|
|
switch vb := b.(type) {
|
|
case int:
|
|
return va == vb
|
|
case float64:
|
|
return float64(va) == vb
|
|
default:
|
|
return false
|
|
}
|
|
case float64:
|
|
switch vb := b.(type) {
|
|
case float64:
|
|
return va == vb
|
|
case int:
|
|
return va == float64(vb)
|
|
default:
|
|
return false
|
|
}
|
|
case bool:
|
|
vb, ok := b.(bool)
|
|
return ok && va == vb
|
|
default:
|
|
return fmt.Sprintf("%v", a) == fmt.Sprintf("%v", b)
|
|
}
|
|
}
|
|
|
|
// RunMemorySaverConformanceTests runs the full conformance suite against MemorySaver.
|
|
func RunMemorySaverConformanceTests(t *testing.T) {
|
|
suite := &ConformanceTestSuite{
|
|
NewCheckpointer: func() BaseCheckpointer {
|
|
return NewMemorySaver()
|
|
},
|
|
}
|
|
suite.RunAll(t)
|
|
}
|
|
|
|
// RunSqliteSaverConformanceTests runs the full conformance suite against SqliteSaver.
|
|
func RunSqliteSaverConformanceTests(t *testing.T, dbPath string) {
|
|
suite := &ConformanceTestSuite{
|
|
NewCheckpointer: func() BaseCheckpointer {
|
|
saver, err := NewSqliteSaver(dbPath)
|
|
if err != nil {
|
|
t.Skipf("SqliteSaver not available: %v", err)
|
|
return nil
|
|
}
|
|
return saver
|
|
},
|
|
}
|
|
suite.RunAll(t)
|
|
}
|
|
|
|
// skipBadTypeFields removes keys that the checkpointer cannot serialize
|
|
// (e.g. channels with unsupported types in SQLite).
|
|
func skipBadTypeFields(data map[string]interface{}, skipKeys ...string) map[string]interface{} {
|
|
result := make(map[string]interface{}, len(data))
|
|
skip := make(map[string]bool, len(skipKeys))
|
|
for _, k := range skipKeys {
|
|
skip[k] = true
|
|
}
|
|
for k, v := range data {
|
|
if !skip[k] {
|
|
result[k] = v
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// NewSqliteSaver creates a SqliteSaver (stub — implement as needed).
|
|
func NewSqliteSaver(dbPath string) (BaseCheckpointer, error) {
|
|
return nil, fmt.Errorf("SqliteSaver not implemented in this package: %s", dbPath)
|
|
}
|
|
|
|
// TestConformance_MemorySaver runs the conformance suite against MemorySaver.
|
|
func TestConformance_MemorySaver(t *testing.T) {
|
|
RunMemorySaverConformanceTests(t)
|
|
}
|
|
|
|
// TestConformance_MemorySaver_SubtestNames tests that all subtest names are set correctly.
|
|
func TestConformance_MemorySaver_SubtestNames(t *testing.T) {
|
|
suite := &ConformanceTestSuite{
|
|
NewCheckpointer: func() BaseCheckpointer { return NewMemorySaver() },
|
|
}
|
|
// Verify that RunAll doesn't panic.
|
|
suite.RunAll(t)
|
|
}
|