Files
ragflow/internal/ingestion/task/tool/compare_dataflow_golden.go
Jack 0dd0ac06f8 Feature: task executor migration to go (#16549)
### Summary

Feature: Integrate parser
2026-07-08 19:08:11 +08:00

289 lines
7.8 KiB
Go

package main
import (
"encoding/json"
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"ragflow/internal/ingestion/task"
)
func main() {
caseDir := flag.String("case-dir", "", "case directory under internal/ingestion/task/testdata/<case_id>")
docID := flag.String("doc-id", "doc-1", "document id")
kbID := flag.String("kb-id", "kb-1", "knowledge base id")
docName := flag.String("doc-name", "sample.md", "document name")
flag.Parse()
if *caseDir == "" {
exitErr(errors.New("--case-dir is required"))
}
inputPath := filepath.Join(*caseDir, "input.json")
outputDir := filepath.Join(*caseDir, "output")
input := mustReadJSONMap(inputPath)
expectedNormalized := mustReadJSONSlice(filepath.Join(outputDir, "normalized_chunks.json"))
expectedProcessed := mustReadJSONSlice(filepath.Join(outputDir, "processed_chunks.json"))
expectedMetadata := mustReadJSONMap(filepath.Join(outputDir, "merged_metadata.json"))
expectedProcessError, hasExpectedProcessError := readOptionalJSONMap(filepath.Join(outputDir, "process_error.json"))
actual, actualErr := runActual(input, *docID, *kbID, *docName)
if hasExpectedProcessError || actualErr != nil {
reportProcessErrorOutcome(expectedProcessError, hasExpectedProcessError, actualErr)
return
}
diffs := make([]string, 0)
diffs = append(diffs, diffValue("normalized_chunks", sanitizeNormalized(expectedNormalized), sanitizeNormalized(actual.NormalizedChunks), false)...)
diffs = append(diffs, diffValue("processed_chunks", sanitizeProcessed(expectedProcessed), sanitizeProcessed(actual.ProcessedChunks), true)...)
diffs = append(diffs, diffValue("merged_metadata", expectedMetadata, actual.MergedMetadata, false)...)
if len(diffs) == 0 {
fmt.Println("validation succeeded")
return
}
fmt.Println("validation failed")
for _, d := range diffs {
fmt.Println(d)
}
os.Exit(1)
}
func runActual(input map[string]any, docID string, kbID string, docName string) (result task.GoldenDataflowResult, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
return task.ProcessPipelineOutputForGolden(input, docID, kbID, docName), nil
}
func reportProcessErrorOutcome(expected map[string]any, hasExpected bool, actualErr error) {
switch {
case hasExpected && actualErr == nil:
fmt.Println("validation failed")
fmt.Printf("expected process error: %v\n", expected)
fmt.Println("actual: processing succeeded")
os.Exit(1)
case !hasExpected && actualErr != nil:
fmt.Println("validation failed")
fmt.Println("expected: processing succeeded")
fmt.Printf("actual error: %v\n", actualErr)
os.Exit(1)
case hasExpected && actualErr != nil:
expectedMsg, _ := expected["message"].(string)
if expectedMsg != "" && strings.Contains(actualErr.Error(), expectedMsg) {
fmt.Println("validation succeeded")
fmt.Printf("expected process error observed: %s\n", expectedMsg)
return
}
fmt.Println("validation failed")
fmt.Printf("expected process error: %v\n", expected)
fmt.Printf("actual error: %v\n", actualErr)
os.Exit(1)
default:
return
}
}
func sanitizeProcessed(chunks []map[string]any) []map[string]any {
out := make([]map[string]any, 0, len(chunks))
for _, chunk := range chunks {
cp := normalizeComparableMap(cloneMap(chunk))
delete(cp, "create_time")
delete(cp, "create_timestamp_flt")
for k := range cp {
if strings.HasPrefix(k, "q_") && strings.HasSuffix(k, "_vec") {
delete(cp, k)
}
}
out = append(out, cp)
}
return out
}
func sanitizeNormalized(chunks []map[string]any) []map[string]any {
out := make([]map[string]any, 0, len(chunks))
for _, chunk := range chunks {
out = append(out, normalizeComparableMap(cloneMap(chunk)))
}
return out
}
func diffValue(path string, expected any, actual any, sortMaps bool) []string {
if reflect.DeepEqual(expected, actual) {
return nil
}
return walkDiff(path, expected, actual, sortMaps)
}
func walkDiff(path string, expected any, actual any, sortMaps bool) []string {
switch exp := expected.(type) {
case map[string]any:
act, ok := actual.(map[string]any)
if !ok {
return []string{fmt.Sprintf("%s: expected map, got %T", path, actual)}
}
keys := unionKeys(exp, act)
diffs := make([]string, 0)
for _, key := range keys {
ev, eok := exp[key]
av, aok := act[key]
switch {
case !eok:
diffs = append(diffs, fmt.Sprintf("%s.%s: unexpected actual value %v", path, key, av))
case !aok:
diffs = append(diffs, fmt.Sprintf("%s.%s: missing actual value, expected %v", path, key, ev))
default:
diffs = append(diffs, walkDiff(path+"."+key, ev, av, sortMaps)...)
}
}
return diffs
case []any:
act, ok := actual.([]any)
if !ok {
return []string{fmt.Sprintf("%s: expected []any, got %T", path, actual)}
}
return diffSlice(path, exp, act, sortMaps)
case []map[string]any:
act, ok := actual.([]map[string]any)
if !ok {
return []string{fmt.Sprintf("%s: expected []map[string]any, got %T", path, actual)}
}
diffs := make([]string, 0)
if len(exp) != len(act) {
diffs = append(diffs, fmt.Sprintf("%s: len mismatch expected=%d actual=%d", path, len(exp), len(act)))
}
n := min(len(exp), len(act))
for i := 0; i < n; i++ {
diffs = append(diffs, walkDiff(fmt.Sprintf("%s[%d]", path, i), exp[i], act[i], sortMaps)...)
}
return diffs
default:
if !reflect.DeepEqual(expected, actual) {
return []string{fmt.Sprintf("%s: expected=%v (%T), actual=%v (%T)", path, expected, expected, actual, actual)}
}
return nil
}
}
func diffSlice(path string, expected []any, actual []any, sortMaps bool) []string {
diffs := make([]string, 0)
if len(expected) != len(actual) {
diffs = append(diffs, fmt.Sprintf("%s: len mismatch expected=%d actual=%d", path, len(expected), len(actual)))
}
n := min(len(expected), len(actual))
for i := 0; i < n; i++ {
diffs = append(diffs, walkDiff(fmt.Sprintf("%s[%d]", path, i), expected[i], actual[i], sortMaps)...)
}
return diffs
}
func unionKeys(a map[string]any, b map[string]any) []string {
set := make(map[string]struct{}, len(a)+len(b))
for k := range a {
set[k] = struct{}{}
}
for k := range b {
set[k] = struct{}{}
}
keys := make([]string, 0, len(set))
for k := range set {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
func cloneMap(in map[string]any) map[string]any {
out := make(map[string]any, len(in))
for k, v := range in {
out[k] = v
}
return out
}
func normalizeComparableMap(in map[string]any) map[string]any {
out := make(map[string]any, len(in))
for k, v := range in {
out[k] = normalizeComparableValue(v)
}
return out
}
func normalizeComparableValue(v any) any {
switch val := v.(type) {
case []string:
out := make([]any, 0, len(val))
for _, item := range val {
out = append(out, item)
}
return out
case []map[string]any:
out := make([]any, 0, len(val))
for _, item := range val {
out = append(out, normalizeComparableMap(item))
}
return out
case map[string]any:
return normalizeComparableMap(val)
default:
return v
}
}
func mustReadJSONMap(path string) map[string]any {
var out map[string]any
mustReadJSON(path, &out)
return out
}
func mustReadJSONSlice(path string) []map[string]any {
var out []map[string]any
mustReadJSON(path, &out)
return out
}
func mustReadJSON(path string, out any) {
data, err := os.ReadFile(path)
if err != nil {
exitErr(err)
}
if err := json.Unmarshal(data, out); err != nil {
exitErr(fmt.Errorf("unmarshal %s: %w", path, err))
}
}
func readOptionalJSONMap(path string) (map[string]any, bool) {
data, err := os.ReadFile(path)
if err != nil {
return nil, false
}
var out map[string]any
if err := json.Unmarshal(data, &out); err != nil {
exitErr(fmt.Errorf("unmarshal %s: %w", path, err))
}
return out, true
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func exitErr(err error) {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}