// Copyright 2025 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 nlp import ( "os" "path/filepath" "reflect" "strings" "testing" ) // TestNewTermWeightDealer tests the constructor func TestNewTermWeightDealer(t *testing.T) { // Test with empty resPath d := NewTermWeightDealer("") if d == nil { t.Fatal("NewTermWeightDealer returned nil") } // Check stop words are initialized if len(d.stopWords) == 0 { t.Error("Stop words not initialized") } // Check stop word exists if _, ok := d.stopWords["请问"]; !ok { t.Error("Expected stop word '请问' not found") } // Test with non-existent resPath (should not panic) d2 := NewTermWeightDealer("/nonexistent/path") if d2 == nil { t.Fatal("NewTermWeightDealer returned nil for non-existent path") } } // TestNewTermWeightDealerWithMockFiles tests with mock dictionary files func TestNewTermWeightDealerWithMockFiles(t *testing.T) { // Create temporary directory with mock files tmpDir := t.TempDir() // Create mock ner.json nerData := `{ "北京": "loca", "腾讯": "corp", "func": "func", "toxic": "toxic" }` if err := os.WriteFile(filepath.Join(tmpDir, "ner.json"), []byte(nerData), 0644); err != nil { t.Fatalf("Failed to create mock ner.json: %v", err) } // Create mock term.freq freqData := "hello\t100\nworld\t200\ntest\t50\n" if err := os.WriteFile(filepath.Join(tmpDir, "term.freq"), []byte(freqData), 0644); err != nil { t.Fatalf("Failed to create mock term.freq: %v", err) } d := NewTermWeightDealer(tmpDir) // Check NE dictionary if ne := d.Ner("北京"); ne != "loca" { t.Errorf("Expected NE 'loca' for '北京', got '%s'", ne) } if ne := d.Ner("腾讯"); ne != "corp" { t.Errorf("Expected NE 'corp' for '腾讯', got '%s'", ne) } // Check DF dictionary if df := d.GetDF(); len(df) != 3 { t.Errorf("Expected 3 entries in DF, got %d", len(df)) } } // TestPretoken tests the pretokenization function func TestPretoken(t *testing.T) { d := NewTermWeightDealer("") tests := []struct { name string txt string num bool stpwd bool expected []string }{ { name: "simple text", txt: "hello world", num: false, stpwd: true, expected: []string{}, // May vary based on tokenizer }, { name: "with stop words", txt: "请问你好吗", num: false, stpwd: true, expected: []string{}, // Stop words should be removed }, { name: "with numbers (num=true)", txt: "123", num: true, stpwd: true, expected: []string{}, // Single digit may be filtered }, { name: "empty text", txt: "", num: false, stpwd: true, expected: []string{}, }, { name: "only punctuation", txt: ",。!?", num: false, stpwd: true, expected: []string{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := d.Pretoken(tt.txt, tt.num, tt.stpwd) // Just check it doesn't panic and returns a slice if result == nil { t.Error("Pretoken returned nil") } }) } } // TestTokenMerge tests token merging func TestTokenMerge(t *testing.T) { d := NewTermWeightDealer("") tests := []struct { name string tks []string expected []string }{ { name: "empty input", tks: []string{}, expected: []string{}, }, { name: "single token", tks: []string{"hello"}, expected: []string{"hello"}, }, { name: "consecutive short tokens", tks: []string{"a", "b", "c"}, expected: []string{"a b c"}, // Should merge }, { name: "mixed tokens", tks: []string{"a", "hello", "b"}, expected: []string{"a", "hello", "b"}, }, { name: "first term single char followed by multi-char", tks: []string{"多", "工位"}, expected: []string{"多 工位"}, // Special case }, { name: "too many short tokens (>=5)", tks: []string{"a", "b", "c", "d", "e", "f"}, expected: []string{"a b", "c d", "e f"}, // Merge in pairs }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := d.TokenMerge(tt.tks) if !reflect.DeepEqual(result, tt.expected) { // Debug: print detailed comparison t.Errorf("TokenMerge(%v) = %v (len=%d), expected %v (len=%d)", tt.tks, result, len(result), tt.expected, len(tt.expected)) for i, r := range result { t.Errorf(" result[%d] = %q (len=%d)", i, r, len(r)) } for i, e := range tt.expected { t.Errorf(" expected[%d] = %q (len=%d)", i, e, len(e)) } } }) } } // TestNer tests named entity recognition func TestNer(t *testing.T) { tmpDir := t.TempDir() // Create mock ner.json nerData := `{ "北京": "loca", "腾讯": "corp", "阿里巴巴": "corp" }` if err := os.WriteFile(filepath.Join(tmpDir, "ner.json"), []byte(nerData), 0644); err != nil { t.Fatalf("Failed to create mock ner.json: %v", err) } d := NewTermWeightDealer(tmpDir) tests := []struct { term string expected string }{ {"北京", "loca"}, {"腾讯", "corp"}, {"阿里巴巴", "corp"}, {"不存在", ""}, {"", ""}, } for _, tt := range tests { t.Run(tt.term, func(t *testing.T) { result := d.Ner(tt.term) if result != tt.expected { t.Errorf("Ner('%s') = '%s', expected '%s'", tt.term, result, tt.expected) } }) } } // TestSplit tests text splitting func TestSplit(t *testing.T) { d := NewTermWeightDealer("") tests := []struct { name string txt string expected []string }{ { name: "simple split", txt: "hello world test", // Consecutive English words ending with letters are merged expected: []string{"hello world test"}, }, { name: "consecutive English words", txt: "machine learning algorithm", expected: []string{"machine learning algorithm"}, // Should merge }, { name: "mixed Chinese and English", txt: "hello 世界 world", // "hello" ends with letter, "世界" doesn't start with letter but doesn't end with letter either expected: []string{"hello", "世界", "world"}, }, { name: "empty string", txt: "", expected: []string{""}, }, { name: "multiple spaces", txt: "hello world", // Multiple spaces are normalized, then merged if both end with letters expected: []string{"hello world"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := d.Split(tt.txt) if !reflect.DeepEqual(result, tt.expected) { t.Errorf("Split('%s') = %v (len=%d), expected %v (len=%d)", tt.txt, result, len(result), tt.expected, len(tt.expected)) for i, r := range result { t.Errorf(" result[%d] = %q", i, r) } for i, e := range tt.expected { t.Errorf(" expected[%d] = %q", i, e) } } }) } } // TestWeights tests weight calculation func TestWeights(t *testing.T) { tmpDir := t.TempDir() // Create mock ner.json nerData := `{ "toxic": "toxic", "func": "func", "corp": "corp", "loca": "loca" }` if err := os.WriteFile(filepath.Join(tmpDir, "ner.json"), []byte(nerData), 0644); err != nil { t.Fatalf("Failed to create mock ner.json: %v", err) } // Create mock term.freq freqData := "hello\t100\nworld\t200\n" if err := os.WriteFile(filepath.Join(tmpDir, "term.freq"), []byte(freqData), 0644); err != nil { t.Fatalf("Failed to create mock term.freq: %v", err) } d := NewTermWeightDealer(tmpDir) t.Run("without preprocess", func(t *testing.T) { tks := []string{"hello", "world", "123"} weights := d.Weights(tks, false) if len(weights) != len(tks) { t.Errorf("Expected %d weights, got %d", len(tks), len(weights)) } // Check weights sum to 1 (normalized) sum := 0.0 for _, tw := range weights { sum += tw.Weight } if sum < 0.99 || sum > 1.01 { t.Errorf("Weights should sum to ~1, got %f", sum) } }) t.Run("with preprocess", func(t *testing.T) { tks := []string{"hello world", "test"} weights := d.Weights(tks, true) // Check it doesn't panic and returns results if weights == nil { t.Error("Weights returned nil") } }) t.Run("empty input", func(t *testing.T) { weights := d.Weights([]string{}, false) if len(weights) != 0 { t.Errorf("Expected empty weights for empty input, got %d", len(weights)) } }) t.Run("ner weight effect", func(t *testing.T) { tmpDir2 := t.TempDir() nerData := `{"toxicterm": "toxic"}` os.WriteFile(filepath.Join(tmpDir2, "ner.json"), []byte(nerData), 0644) d2 := NewTermWeightDealer(tmpDir2) tks := []string{"toxicterm", "normal"} weights := d2.Weights(tks, false) if len(weights) != 2 { t.Fatalf("Expected 2 weights, got %d", len(weights)) } // toxicterm should have higher weight (nerWeight=2) if weights[0].Weight <= weights[1].Weight { t.Error("Expected toxicterm to have higher weight than normal term") } }) } // TestWeightsWithNER tests NER type weight effects func TestWeightsWithNER(t *testing.T) { tmpDir := t.TempDir() // Create mock ner.json with all types nerData := `{ "toxic_word": "toxic", "func_word": "func", "corp_name": "corp", "location": "loca", "school": "sch", "stock": "stock", "firstname": "firstnm" }` if err := os.WriteFile(filepath.Join(tmpDir, "ner.json"), []byte(nerData), 0644); err != nil { t.Fatalf("Failed to create mock ner.json: %v", err) } d := NewTermWeightDealer(tmpDir) tests := []struct { term string expectedType string }{ {"toxic_word", "toxic"}, {"func_word", "func"}, {"corp_name", "corp"}, {"location", "loca"}, {"school", "sch"}, {"stock", "stock"}, {"firstname", "firstnm"}, } for _, tt := range tests { t.Run(tt.term, func(t *testing.T) { ne := d.Ner(tt.term) if ne != tt.expectedType { t.Errorf("Ner('%s') = '%s', expected '%s'", tt.term, ne, tt.expectedType) } }) } } // TestGetters tests the getter methods func TestGetters(t *testing.T) { tmpDir := t.TempDir() // Create mock files nerData := `{"test": "type"}` os.WriteFile(filepath.Join(tmpDir, "ner.json"), []byte(nerData), 0644) os.WriteFile(filepath.Join(tmpDir, "term.freq"), []byte("word\t10\n"), 0644) d := NewTermWeightDealer(tmpDir) t.Run("GetStopWords", func(t *testing.T) { sw := d.GetStopWords() if len(sw) == 0 { t.Error("GetStopWords returned empty map") } if _, ok := sw["请问"]; !ok { t.Error("Expected stop word '请问' not in map") } }) t.Run("GetNE", func(t *testing.T) { ne := d.GetNE() if len(ne) != 1 { t.Errorf("Expected 1 NE entry, got %d", len(ne)) } if ne["test"] != "type" { t.Error("NE dictionary content incorrect") } }) t.Run("GetDF", func(t *testing.T) { df := d.GetDF() if len(df) != 1 { t.Errorf("Expected 1 DF entry, got %d", len(df)) } if df["word"] != 10 { t.Error("DF dictionary content incorrect") } }) } // TestLoadDict tests dictionary loading func TestLoadDict(t *testing.T) { t.Run("load with frequency", func(t *testing.T) { tmpDir := t.TempDir() content := "word1\t100\nword2\t200\nword3\t300\n" fn := filepath.Join(tmpDir, "test.freq") os.WriteFile(fn, []byte(content), 0644) dict := loadDict(fn) if len(dict) != 3 { t.Errorf("Expected 3 entries, got %d", len(dict)) } if dict["word1"] != 100 { t.Errorf("Expected word1=100, got %d", dict["word1"]) } }) t.Run("load without frequency (set mode)", func(t *testing.T) { tmpDir := t.TempDir() content := "word1\nword2\nword3\n" fn := filepath.Join(tmpDir, "test.freq") os.WriteFile(fn, []byte(content), 0644) dict := loadDict(fn) if len(dict) != 3 { t.Errorf("Expected 3 entries, got %d", len(dict)) } // All values should be 0 in set mode for k, v := range dict { if v != 0 { t.Errorf("Expected %s=0 in set mode, got %d", k, v) } } }) t.Run("load non-existent file", func(t *testing.T) { dict := loadDict("/nonexistent/file.txt") if dict == nil { t.Error("loadDict should return empty map, not nil") } if len(dict) != 0 { t.Error("loadDict should return empty map for non-existent file") } }) t.Run("load with malformed lines", func(t *testing.T) { tmpDir := t.TempDir() content := "word1\t100\n\n\nword2\tnotanumber\nword3" fn := filepath.Join(tmpDir, "test.freq") os.WriteFile(fn, []byte(content), 0644) dict := loadDict(fn) // Should handle empty lines and invalid numbers gracefully if len(dict) < 1 { t.Error("Should handle malformed lines gracefully") } }) } // TestWeightsNormalization tests weight normalization func TestWeightsNormalization(t *testing.T) { d := NewTermWeightDealer("") tests := []struct { name string tks []string }{ { name: "single token", tks: []string{"hello"}, }, { name: "multiple tokens", tks: []string{"hello", "world", "test"}, }, { name: "many tokens", tks: []string{"a", "b", "c", "d", "e"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { weights := d.Weights(tt.tks, false) if len(weights) != len(tt.tks) { t.Fatalf("Expected %d weights, got %d", len(tt.tks), len(weights)) } // Sum should be approximately 1 sum := 0.0 for _, tw := range weights { sum += tw.Weight // Individual weights should be non-negative if tw.Weight < 0 { t.Errorf("Weight for '%s' is negative: %f", tw.Term, tw.Weight) } } if sum < 0.99 || sum > 1.01 { t.Errorf("Weights sum to %f, expected ~1.0", sum) } }) } } // TestSplitWithNER tests Split with NER considerations func TestSplitWithNER(t *testing.T) { tmpDir := t.TempDir() // Create mock ner.json nerData := `{ "function": "func" }` os.WriteFile(filepath.Join(tmpDir, "ner.json"), []byte(nerData), 0644) d := NewTermWeightDealer(tmpDir) t.Run("func type should not merge", func(t *testing.T) { // If one of the words has NE type "func", they should not merge result := d.Split("hello function") // "hello" and "function" should not merge because function has type "func" if len(result) != 2 { t.Logf("Result: %v", result) } }) } // BenchmarkWeights benchmarks the Weights function func BenchmarkWeights(b *testing.B) { d := NewTermWeightDealer("") tks := []string{"hello", "world", "this", "is", "a", "test", "of", "term", "weights", "calculation"} b.ResetTimer() for i := 0; i < b.N; i++ { d.Weights(tks, false) } } // BenchmarkTokenMerge benchmarks the TokenMerge function func BenchmarkTokenMerge(b *testing.B) { d := NewTermWeightDealer("") tks := []string{"a", "b", "c", "d", "e", "hello", "world", "x", "y", "z"} b.ResetTimer() for i := 0; i < b.N; i++ { d.TokenMerge(tks) } } // TestTermWeightStructure tests the TermWeight struct func TestTermWeightStructure(t *testing.T) { tw := TermWeight{ Term: "test", Weight: 0.5, } if tw.Term != "test" { t.Error("Term field incorrect") } if tw.Weight != 0.5 { t.Error("Weight field incorrect") } } // TestIntegration tests an integrated workflow func TestIntegration(t *testing.T) { tmpDir := t.TempDir() // Create mock dictionaries nerData := `{ "北京": "loca", "腾讯": "corp" }` os.WriteFile(filepath.Join(tmpDir, "ner.json"), []byte(nerData), 0644) os.WriteFile(filepath.Join(tmpDir, "term.freq"), []byte("北京\t1000\n腾讯\t500\n"), 0644) d := NewTermWeightDealer(tmpDir) // Full workflow: text -> split -> pretoken -> token_merge -> weights text := "北京 腾讯 公司" // Step 1: Split splitted := d.Split(text) if len(splitted) == 0 { t.Fatal("Split returned empty result") } // Step 2: Pretoken var allTokens []string for _, s := range splitted { tokens := d.Pretoken(s, true, true) allTokens = append(allTokens, tokens...) } // Step 3: Token merge merged := d.TokenMerge(allTokens) // Step 4: Calculate weights weights := d.Weights(merged, false) // Verify results if len(weights) == 0 && len(merged) > 0 { t.Error("Weights calculation failed") } // Check weights sum to 1 sum := 0.0 for _, w := range weights { sum += w.Weight } if sum < 0.99 || sum > 1.01 { t.Errorf("Final weights sum to %f, expected ~1.0", sum) } } // TestWeightsEdgeCases tests edge cases for weight calculation func TestWeightsEdgeCases(t *testing.T) { d := NewTermWeightDealer("") t.Run("numbers pattern", func(t *testing.T) { tks := []string{"123,45", "abc"} weights := d.Weights(tks, false) if len(weights) != 2 { t.Fatalf("Expected 2 weights, got %d", len(weights)) } // Numbers should get nerWeight=2 }) t.Run("short letters pattern", func(t *testing.T) { tks := []string{"ab", "abc"} weights := d.Weights(tks, false) if len(weights) != 2 { t.Fatalf("Expected 2 weights, got %d", len(weights)) } }) t.Run("letter pattern with spaces", func(t *testing.T) { tks := []string{"hello world test"} weights := d.Weights(tks, true) // Should not panic if weights == nil { t.Error("Weights returned nil for letter pattern") } }) } // TestPretokenWithNumbers tests pretoken with num parameter func TestPretokenWithNumbers(t *testing.T) { d := NewTermWeightDealer("") t.Run("num=false filters single digits", func(t *testing.T) { result := d.Pretoken("5", false, true) // Single digit should be filtered when num=false found := false for _, r := range result { if r == "5" { found = true break } } if found { t.Error("Single digit should be filtered when num=false") } }) t.Run("num=true keeps single digits", func(t *testing.T) { result := d.Pretoken("5 123", true, true) // Check at least something is returned if len(result) == 0 { t.Log("Single digit may still be filtered by other rules") } }) } // TestPretokenStopWords tests pretoken with stpwd parameter func TestPretokenStopWords(t *testing.T) { d := NewTermWeightDealer("") t.Run("stpwd=true removes stop words", func(t *testing.T) { result := d.Pretoken("请问", true, true) // "请问" is a stop word for _, r := range result { if r == "请问" { t.Error("Stop word should be removed when stpwd=true") } } }) t.Run("stpwd=false keeps stop words", func(t *testing.T) { result := d.Pretoken("请问", true, false) // With tokenizer, this might still filter it _ = result }) } // TestTokenMergeEdgeCases tests edge cases for token merging func TestTokenMergeEdgeCases(t *testing.T) { d := NewTermWeightDealer("") t.Run("nil input", func(t *testing.T) { result := d.TokenMerge(nil) if len(result) != 0 { t.Error("TokenMerge(nil) should return empty slice") } }) t.Run("empty strings in input", func(t *testing.T) { result := d.TokenMerge([]string{"", "a", "", "b", ""}) // Empty strings should be filtered for _, r := range result { if r == "" { t.Error("Empty strings should be filtered") } } }) t.Run("exactly 4 short tokens", func(t *testing.T) { // 4 short tokens should be merged as one group (not split into pairs) result := d.TokenMerge([]string{"a", "b", "c", "d"}) expected := []string{"a b c d"} if !reflect.DeepEqual(result, expected) { t.Errorf("Expected %v, got %v", expected, result) } }) t.Run("exactly 5 short tokens", func(t *testing.T) { // 5 short tokens should be split into pairs result := d.TokenMerge([]string{"a", "b", "c", "d", "e"}) // Should be: a b, c d (e is left? depends on implementation) if len(result) < 2 { t.Errorf("Expected at least 2 groups for 5 tokens, got %d: %v", len(result), result) } }) } // TestSplitEdgeCases tests edge cases for splitting func TestSplitEdgeCases(t *testing.T) { d := NewTermWeightDealer("") t.Run("tabs and spaces", func(t *testing.T) { result := d.Split("hello\tworld\t\ttest") // Tabs should be normalized to single space hasTab := false for _, r := range result { if strings.Contains(r, "\t") { hasTab = true break } } if hasTab { t.Error("Tabs should be normalized") } }) t.Run("consecutive English with different NE types", func(t *testing.T) { tmpDir := t.TempDir() nerData := `{ "hello": "func", "world": "corp" }` os.WriteFile(filepath.Join(tmpDir, "ner.json"), []byte(nerData), 0644) d2 := NewTermWeightDealer(tmpDir) result := d2.Split("hello world") // Both have NE types, so they should NOT merge if len(result) != 2 { t.Errorf("Expected 2 tokens when both have NE types, got %d: %v", len(result), result) } }) }