mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-06 07:34:04 +08:00
186 lines
5.6 KiB
Go
186 lines
5.6 KiB
Go
|
|
//
|
|||
|
|
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
|
|||
|
|
//
|
|||
|
|
// 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 chunk
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"reflect"
|
|||
|
|
"regexp"
|
|||
|
|
"testing"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func TestNormalizeTextNewlines(t *testing.T) {
|
|||
|
|
tests := []struct {
|
|||
|
|
in, want string
|
|||
|
|
}{
|
|||
|
|
{"", ""},
|
|||
|
|
{"a\nb", "a\nb"},
|
|||
|
|
{"a\r\nb", "a\nb"},
|
|||
|
|
{"a\rb", "a\nb"},
|
|||
|
|
{"a\r\nb\rc", "a\nb\nc"},
|
|||
|
|
}
|
|||
|
|
for _, tc := range tests {
|
|||
|
|
if got := NormalizeTextNewlines(tc.in); got != tc.want {
|
|||
|
|
t.Errorf("NormalizeTextNewlines(%q) = %q, want %q", tc.in, got, tc.want)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestHasWrappedDelimiter(t *testing.T) {
|
|||
|
|
if HasWrappedDelimiter("") {
|
|||
|
|
t.Fatal("empty should be false")
|
|||
|
|
}
|
|||
|
|
if HasWrappedDelimiter("!;") {
|
|||
|
|
t.Fatal("bare chars should be false")
|
|||
|
|
}
|
|||
|
|
if !HasWrappedDelimiter("`##`") {
|
|||
|
|
t.Fatal("wrapped should be true")
|
|||
|
|
}
|
|||
|
|
if !HasWrappedDelimiter("\n`##`;") {
|
|||
|
|
t.Fatal("mixed should be true")
|
|||
|
|
}
|
|||
|
|
if !HasWrappedDelimiter("`;`") {
|
|||
|
|
t.Fatal("wrapped single char should be true")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestParseDelimiterField(t *testing.T) {
|
|||
|
|
tests := []struct {
|
|||
|
|
name string
|
|||
|
|
field string
|
|||
|
|
want []string
|
|||
|
|
}{
|
|||
|
|
{"empty", "", nil},
|
|||
|
|
{"single bare", "!", []string{"!"}},
|
|||
|
|
{"bare pair", "!?", []string{"!", "?"}},
|
|||
|
|
{"space", " ", []string{" "}},
|
|||
|
|
{"newline", "\n", []string{"\n"}},
|
|||
|
|
{"crlf field", "\r\n", []string{"\n"}},
|
|||
|
|
{"bare cr", "\r", []string{"\n"}},
|
|||
|
|
{"wrapped end", "`end`", []string{"end"}},
|
|||
|
|
{"wrapped hierarchy", "`###``##``#`", []string{"###", "##", "#"}},
|
|||
|
|
{"tooltip example", "\n`##`;", []string{"##", "\n", ";"}},
|
|||
|
|
{"dedupe", "`a`a`a`", []string{"a"}},
|
|||
|
|
{"wrapped double newline", "`\n\n`", []string{"\n\n"}},
|
|||
|
|
{"crlf wrapped", "`\r\n`", []string{"\n"}},
|
|||
|
|
{"shipped default", "\n!?;。;!?", []string{"\n", "!", "?", ";", "。", ";", "!", "?"}},
|
|||
|
|
{"chinese", "。;", []string{"。", ";"}},
|
|||
|
|
{"mixed order equal length", "`##`#\n", []string{"##", "#", "\n"}},
|
|||
|
|
{"empty backticks bare", "``", []string{"`"}},
|
|||
|
|
{"double space bare dedupe", " ", []string{" "}},
|
|||
|
|
{"wrapped double space", "` `", []string{" "}},
|
|||
|
|
}
|
|||
|
|
for _, tc := range tests {
|
|||
|
|
t.Run(tc.name, func(t *testing.T) {
|
|||
|
|
got := ParseDelimiterField(tc.field)
|
|||
|
|
if !reflect.DeepEqual(got, tc.want) {
|
|||
|
|
t.Errorf("ParseDelimiterField(%q) = %#v, want %#v", tc.field, got, tc.want)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestCompileDelimiterPattern(t *testing.T) {
|
|||
|
|
if CompileDelimiterPattern(nil) != nil {
|
|||
|
|
t.Fatal("empty list should yield nil")
|
|||
|
|
}
|
|||
|
|
if CompileDelimiterPattern([]string{}) != nil {
|
|||
|
|
t.Fatal("empty slice should yield nil")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pat := CompileDelimiterPattern([]string{"##", "#"})
|
|||
|
|
if pat == nil {
|
|||
|
|
t.Fatal("expected pattern")
|
|||
|
|
}
|
|||
|
|
if got := pat.FindString("###"); got != "##" {
|
|||
|
|
t.Errorf("longest match: got %q, want ##", got)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Metacharacters match literally.
|
|||
|
|
for _, ch := range []string{".", "(", "?", "+"} {
|
|||
|
|
p := CompileDelimiterPattern([]string{ch})
|
|||
|
|
if p == nil || !p.MatchString(ch) {
|
|||
|
|
t.Errorf("pattern for %q should match itself", ch)
|
|||
|
|
}
|
|||
|
|
if ch != "." && p.MatchString("z") {
|
|||
|
|
t.Errorf("pattern for %q should not match z", ch)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestCompileDelimiterPatternShippedDefault(t *testing.T) {
|
|||
|
|
pat := CompileDelimiterPattern(ParseDelimiterField("\n!?;。;!?"))
|
|||
|
|
if pat == nil {
|
|||
|
|
t.Fatal("expected pattern")
|
|||
|
|
}
|
|||
|
|
for _, ch := range []string{"\n", "!", "?", ";", "。", ";", "!", "?"} {
|
|||
|
|
if !pat.MatchString(ch) {
|
|||
|
|
t.Errorf("default pattern must match %q", ch)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if pat.MatchString("a") {
|
|||
|
|
t.Error("default pattern must not match a")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestCompileDelimiterPatternCaseSensitive(t *testing.T) {
|
|||
|
|
pat := CompileDelimiterPattern(ParseDelimiterField("a"))
|
|||
|
|
parts := regexp.MustCompile("("+pat.String()+")").Split("AaBb", -1)
|
|||
|
|
// Split removes matches; "A" + "Bb" with "a" consumed.
|
|||
|
|
if len(parts) < 2 {
|
|||
|
|
t.Fatalf("parts=%v", parts)
|
|||
|
|
}
|
|||
|
|
// Only lowercase a splits.
|
|||
|
|
re := regexp.MustCompile("(" + pat.String() + ")")
|
|||
|
|
got := re.Split("AaBb", -1)
|
|||
|
|
want := []string{"A", "Bb"}
|
|||
|
|
// re.Split in Go does not keep delimiters; with capturing group behavior
|
|||
|
|
// differs — just check case sensitivity via FindAll.
|
|||
|
|
matches := pat.FindAllString("AaBb", -1)
|
|||
|
|
if !reflect.DeepEqual(matches, []string{"a"}) {
|
|||
|
|
t.Errorf("matches=%v want [a]; split parts=%v want-ish %v", matches, got, want)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestCompileDelimiterListPattern(t *testing.T) {
|
|||
|
|
// Bare list entries are ignored (TokenChunker list API).
|
|||
|
|
if CompileDelimiterListPattern([]string{"\n", "!"}) != nil {
|
|||
|
|
t.Fatal("bare list entries should not produce a pattern")
|
|||
|
|
}
|
|||
|
|
pat := CompileDelimiterListPattern([]string{"`##`", "`#`"})
|
|||
|
|
if pat == nil {
|
|||
|
|
t.Fatal("expected pattern from wrapped list entries")
|
|||
|
|
}
|
|||
|
|
if got := pat.FindString("###"); got != "##" {
|
|||
|
|
t.Errorf("got %q want ##", got)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Verify unescaped rune length sorting order: "a.b" (3 runes) vs "ab" (2 runes with meta chars)
|
|||
|
|
patMeta := CompileDelimiterListPattern([]string{"`a`", "`a.b`"})
|
|||
|
|
if patMeta.String() != `a\.b|a` {
|
|||
|
|
t.Errorf("got pattern %q, want %q", patMeta.String(), `a\.b|a`)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestHasCustomDelimiterList(t *testing.T) {
|
|||
|
|
if HasCustomDelimiterList([]string{"\n", "!"}) {
|
|||
|
|
t.Fatal("bare list should be false")
|
|||
|
|
}
|
|||
|
|
if !HasCustomDelimiterList([]string{"\n", "`##`"}) {
|
|||
|
|
t.Fatal("wrapped list entry should be true")
|
|||
|
|
}
|
|||
|
|
}
|