mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-15 13:14:28 +08:00
test(parser): add shared golden-doc + alignment helpers
Extract the alignment scaffolding so the format-specific PRs (text&code,
markdown golden, HTML) don't conflict on align_test.go.
Adds (test-only, no production behavior change):
- GoldenDoc{Meta,Items} and parseGolden: tolerant of both the legacy bare
array and the new {meta, items} golden format.
- LoadGoldenDoc, AcceptedDivergences(meta), FilterOutDocTypes(items, drop):
meta-driven divergence handling, no hardcoded divergence lists.
- TextCodeAlignOptions / DefaultTextCodeDelimiter, HTMLAlignOptions /
StripHTMLHeadingMarker: normalizer presets reused by the alignment tests.
- LoadGolden now tolerates the {meta, items} format.
Stacks on #18014 (the align_test.go framework is already in main).
This commit is contained in:
@@ -188,19 +188,104 @@ func diffReport(g, p string) string {
|
||||
return "alignment mismatch after normalization:\n--- GO ---\n" + g + "\n--- PY ---\n" + p
|
||||
}
|
||||
|
||||
// LoadGolden reads a Python golden JSON file (a JSON list of item objects)
|
||||
// produced by the Python flow parser for the same input.
|
||||
// GoldenDoc is a {meta, items} Python golden baseline. Meta records how the
|
||||
// baseline was produced (generator, sample, delimiter, accepted divergences)
|
||||
// so it stays reproducible without a committed generator script; Items is the
|
||||
// list of parsed output items compared against Go's parser.
|
||||
type GoldenDoc struct {
|
||||
Meta map[string]any
|
||||
Items []map[string]any
|
||||
}
|
||||
|
||||
// parseGolden unmarshals a golden file that may be either a bare JSON array of
|
||||
// items (legacy format) or a {meta, items} document (current format). It
|
||||
// returns the full document either way. Tolerant parsing keeps older
|
||||
// callers/tests working after the format gained a meta block.
|
||||
func parseGolden(t *testing.T, data []byte) *GoldenDoc {
|
||||
t.Helper()
|
||||
var doc GoldenDoc
|
||||
if err := json.Unmarshal(data, &doc); err == nil && doc.Items != nil {
|
||||
return &doc
|
||||
}
|
||||
// Legacy flat-array format: treat the whole file as the items list.
|
||||
var items []map[string]any
|
||||
if err := json.Unmarshal(data, &items); err != nil {
|
||||
t.Fatalf("parse golden: %v", err)
|
||||
}
|
||||
return &GoldenDoc{Items: items}
|
||||
}
|
||||
|
||||
// LoadGolden reads a Python golden JSON file and returns its items. The file
|
||||
// may be a bare array (legacy) or a {meta, items} document; either way only
|
||||
// the items are returned, so existing callers keep working unchanged.
|
||||
func LoadGolden(t *testing.T, path string) []map[string]any {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("load golden %s: %v", path, err)
|
||||
}
|
||||
var items []map[string]any
|
||||
if err := json.Unmarshal(data, &items); err != nil {
|
||||
t.Fatalf("parse golden %s: %v", path, err)
|
||||
doc := parseGolden(t, data)
|
||||
if len(doc.Items) == 0 {
|
||||
t.Fatalf("golden %s has no items", path)
|
||||
}
|
||||
return items
|
||||
return doc.Items
|
||||
}
|
||||
|
||||
// LoadGoldenDoc reads a Python golden JSON file and returns the full
|
||||
// {meta, items} document, including the meta block. Used by tests that drive
|
||||
// behavior from the golden's metadata (e.g. accepted_divergences).
|
||||
func LoadGoldenDoc(t *testing.T, path string) *GoldenDoc {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("load golden %s: %v", path, err)
|
||||
}
|
||||
doc := parseGolden(t, data)
|
||||
if len(doc.Items) == 0 {
|
||||
t.Fatalf("golden %s has no items", path)
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
// AcceptedDivergences returns the doc_type_kwd values the golden baseline
|
||||
// declares as accepted representation differences (e.g. "table"/"image"), so
|
||||
// the comparison can ignore them on both sides. Driven entirely by the
|
||||
// golden's meta block — the test holds no hardcoded divergence list.
|
||||
func AcceptedDivergences(meta map[string]any) []string {
|
||||
raw, ok := meta["accepted_divergences"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
list, ok := raw.([]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(list))
|
||||
for _, e := range list {
|
||||
if s, ok := e.(string); ok {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// FilterOutDocTypes returns the items whose doc_type_kwd is NOT in drop. Used
|
||||
// to exclude the meta-declared accepted divergences from the comparison.
|
||||
func FilterOutDocTypes(items []map[string]any, drop []string) []map[string]any {
|
||||
if len(drop) == 0 {
|
||||
return items
|
||||
}
|
||||
banned := make(map[string]bool, len(drop))
|
||||
for _, d := range drop {
|
||||
banned[d] = true
|
||||
}
|
||||
out := make([]map[string]any, 0, len(items))
|
||||
for _, it := range items {
|
||||
if v, _ := it["doc_type_kwd"].(string); !banned[v] {
|
||||
out = append(out, it)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// MarkdownAlignOptions returns the normalizer preset for Markdown. The order
|
||||
@@ -235,3 +320,73 @@ func MarkdownAlignOptions(delimiter string) AlignOptions {
|
||||
// DefaultMarkdownDelimiter is the flow parser's default Markdown delimiter
|
||||
// set, used when generating/loading the golden baseline.
|
||||
const DefaultMarkdownDelimiter = "\n!?;。;!?"
|
||||
|
||||
// DefaultTextCodeDelimiter is the flow parser's default text&code delimiter
|
||||
// set, used when generating/loading the golden baseline.
|
||||
const DefaultTextCodeDelimiter = "\n!?;。;!?"
|
||||
|
||||
// TextCodeAlignOptions returns the normalizer preset for the text&code family.
|
||||
// Unlike markdown it has no syntax or HTML markup to strip, so only the
|
||||
// delimiter-set replacement and whitespace collapse run:
|
||||
// - WithDelimiterStrip: replace the delimiter runes Python consumes at split
|
||||
// points (kept inline on the Go side via keep_delimiters=True) with a space
|
||||
// so both sides keep the same token separation.
|
||||
// - CollapseWhitespace last: folds the introduced spaces and any inter-segment
|
||||
// gaps into single spaces.
|
||||
//
|
||||
// Reused by every text&code alignment test; shares CompareAlignment with the
|
||||
// other format presets (MarkdownAlignOptions).
|
||||
func TextCodeAlignOptions(delimiter string) AlignOptions {
|
||||
return AlignOptions{
|
||||
Normalizers: []Normalizer{
|
||||
WithDelimiterStrip(delimiter),
|
||||
CollapseWhitespace(),
|
||||
},
|
||||
ItemKey: "text",
|
||||
}
|
||||
}
|
||||
|
||||
// htmlHeadingMarkerRE matches a leading ATX heading marker so Python's
|
||||
// "# Title" (deepdoc merge_block_text prefixes h1–h6 with "# ") can be
|
||||
// normalized to Go's clean heading text.
|
||||
var htmlHeadingMarkerRE = regexp.MustCompile(`(?m)^#{1,6}\s+`)
|
||||
|
||||
// StripHTMLHeadingMarker returns a Normalizer that removes a leading ATX
|
||||
// heading marker ("#"/"##"/…) from a line. Python's HTML flow parser
|
||||
// (deepdoc parser.py merge_block_text) prefixes h1–h6 sections with "# ",
|
||||
// while the Go HTML parser emits clean heading text. This is a representation
|
||||
// difference, not a content divergence, so it is stripped before comparing.
|
||||
// It must run before CollapseWhitespace because the marker relies on the line
|
||||
// start.
|
||||
func StripHTMLHeadingMarker() Normalizer {
|
||||
return func(s string) string {
|
||||
return htmlHeadingMarkerRE.ReplaceAllString(s, "")
|
||||
}
|
||||
}
|
||||
|
||||
// HTMLAlignOptions returns the normalizer preset for HTML. Order matters:
|
||||
// - StripHTMLHeadingMarker first: drops the "# " Python prefixes from h1–h6
|
||||
// sections (relies on the line start, so before CollapseWhitespace).
|
||||
// - StripHTMLTags next: replace table/HTML tags with a space (not delete) so
|
||||
// adjacent cell text does not fuse, e.g. "<td>A</td><td>B</td>" → "A B".
|
||||
// - CollapseWhitespace last: folds all remaining internal whitespace (the
|
||||
// inter-tag gaps of a table, the space from any heading-marker removal)
|
||||
// into single spaces.
|
||||
//
|
||||
// No WithDelimiterStrip: HTML is emitted one item per block (no delimiter
|
||||
// split), and the Python flow chunks HTML at 512 tokens — boundaries differ,
|
||||
// but CompareAlignment concatenates normalized text boundary-agnostically, so
|
||||
// only the concatenated content (order preserved) is compared.
|
||||
//
|
||||
// Reused by the HTML alignment test; shares CompareAlignment with the other
|
||||
// format presets (MarkdownAlignOptions, TextCodeAlignOptions).
|
||||
func HTMLAlignOptions() AlignOptions {
|
||||
return AlignOptions{
|
||||
Normalizers: []Normalizer{
|
||||
StripHTMLHeadingMarker(),
|
||||
StripHTMLTags(),
|
||||
CollapseWhitespace(),
|
||||
},
|
||||
ItemKey: "text",
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user