Go-based dictionary backend providing word validation, definitions, synonyms, and cross-language translation for English, French, and Portuguese. Designed as a self-hosted replacement for Wordnik, backing GogoBee. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
188 lines
5.2 KiB
Go
188 lines
5.2 KiB
Go
package loader
|
|
|
|
import (
|
|
"database/sql"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"dreamdict/internal/dictionary"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
func setupWiktTestDB(t *testing.T) *sql.DB {
|
|
t.Helper()
|
|
db, err := sql.Open("sqlite", ":memory:?_pragma=foreign_keys(on)")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
db.SetMaxOpenConns(1)
|
|
if err := dictionary.BootstrapSchema(db); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return db
|
|
}
|
|
|
|
func writeJSONL(t *testing.T, dir, name, content string) string {
|
|
t.Helper()
|
|
path := filepath.Join(dir, name)
|
|
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func TestFormOfFiltering(t *testing.T) {
|
|
db := setupWiktTestDB(t)
|
|
defer db.Close()
|
|
|
|
// Seed a word so definitions can reference it
|
|
if _, err := db.Exec("INSERT INTO words (word, lang) VALUES ('run', 'en')"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
jsonl := `{"word":"run","pos":"verb","lang_code":"en","senses":[{"glosses":["to move quickly"],"tags":[]},{"glosses":["past tense of run"],"tags":["form-of"]},{"glosses":["alternative form"],"tags":["alt-of"]}],"synonyms":[],"translations":[]}` + "\n"
|
|
path := writeJSONL(t, dir, "test.jsonl", jsonl)
|
|
|
|
if err := loadWiktionary(db, path, "en"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Only the first sense should be loaded — form-of and alt-of should be filtered
|
|
var count int
|
|
if err := db.QueryRow("SELECT COUNT(*) FROM definitions").Scan(&count); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 1 {
|
|
t.Errorf("expected 1 definition (form-of filtered), got %d", count)
|
|
}
|
|
|
|
// Verify the correct gloss was kept
|
|
var gloss string
|
|
if err := db.QueryRow("SELECT gloss FROM definitions").Scan(&gloss); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if gloss != "to move quickly" {
|
|
t.Errorf("expected 'to move quickly', got %q", gloss)
|
|
}
|
|
}
|
|
|
|
func TestBrazilFiltering(t *testing.T) {
|
|
db := setupWiktTestDB(t)
|
|
defer db.Close()
|
|
|
|
if _, err := db.Exec("INSERT INTO words (word, lang) VALUES ('autocarro', 'pt-PT')"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
jsonl := `{"word":"autocarro","pos":"noun","lang_code":"pt","senses":[{"glosses":["a bus (Portugal)"],"tags":["Portugal"]},{"glosses":["a bus (Brazil)"],"tags":["Brazil"]},{"glosses":["a vehicle for transport"],"tags":[]}],"synonyms":[],"translations":[]}` + "\n"
|
|
path := writeJSONL(t, dir, "test.jsonl", jsonl)
|
|
|
|
if err := loadWiktionary(db, path, "pt-PT"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Brazil-tagged sense should be filtered; Portugal-tagged and untagged should remain
|
|
var count int
|
|
if err := db.QueryRow("SELECT COUNT(*) FROM definitions").Scan(&count); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 2 {
|
|
t.Errorf("expected 2 definitions (Brazil filtered), got %d", count)
|
|
}
|
|
}
|
|
|
|
func TestBrazilianPortugueseTagFiltering(t *testing.T) {
|
|
db := setupWiktTestDB(t)
|
|
defer db.Close()
|
|
|
|
if _, err := db.Exec("INSERT INTO words (word, lang) VALUES ('ônibus', 'pt-PT')"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
jsonl := `{"word":"ônibus","pos":"noun","lang_code":"pt","senses":[{"glosses":["a bus"],"tags":["Brazilian Portuguese"]}],"synonyms":[],"translations":[]}` + "\n"
|
|
path := writeJSONL(t, dir, "test.jsonl", jsonl)
|
|
|
|
if err := loadWiktionary(db, path, "pt-PT"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var count int
|
|
if err := db.QueryRow("SELECT COUNT(*) FROM definitions").Scan(&count); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 0 {
|
|
t.Errorf("expected 0 definitions (Brazilian Portuguese filtered), got %d", count)
|
|
}
|
|
}
|
|
|
|
func TestGlossFiltering(t *testing.T) {
|
|
db := setupWiktTestDB(t)
|
|
defer db.Close()
|
|
|
|
if _, err := db.Exec("INSERT INTO words (word, lang) VALUES ('test', 'en')"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
jsonl := `{"word":"test","pos":"noun","lang_code":"en","senses":[{"glosses":["a valid definition"],"tags":[]},{"glosses":["(archaic form)"],"tags":[]},{"glosses":[""],"tags":[]}],"synonyms":[],"translations":[]}` + "\n"
|
|
path := writeJSONL(t, dir, "test.jsonl", jsonl)
|
|
|
|
if err := loadWiktionary(db, path, "en"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Only "a valid definition" should pass — parenthetical and empty filtered
|
|
var count int
|
|
if err := db.QueryRow("SELECT COUNT(*) FROM definitions").Scan(&count); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if count != 1 {
|
|
t.Errorf("expected 1 definition (gloss filtering), got %d", count)
|
|
}
|
|
}
|
|
|
|
func TestHasTag(t *testing.T) {
|
|
tests := []struct {
|
|
tags []string
|
|
targets []string
|
|
want bool
|
|
}{
|
|
{[]string{"form-of", "verb"}, []string{"form-of"}, true},
|
|
{[]string{"verb"}, []string{"form-of", "alt-of"}, false},
|
|
{[]string{"alt-of"}, []string{"form-of", "alt-of"}, true},
|
|
{nil, []string{"form-of"}, false},
|
|
{[]string{}, []string{"form-of"}, false},
|
|
}
|
|
for _, tt := range tests {
|
|
got := hasTag(tt.tags, tt.targets...)
|
|
if got != tt.want {
|
|
t.Errorf("hasTag(%v, %v) = %v, want %v", tt.tags, tt.targets, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHasBrazilTag(t *testing.T) {
|
|
tests := []struct {
|
|
tags []string
|
|
want bool
|
|
}{
|
|
{[]string{"Brazil"}, true},
|
|
{[]string{"Brazilian Portuguese"}, true},
|
|
{[]string{"Portugal"}, false},
|
|
{[]string{"European Portuguese"}, false},
|
|
{nil, false},
|
|
{[]string{}, false},
|
|
}
|
|
for _, tt := range tests {
|
|
got := hasBrazilTag(tt.tags)
|
|
if got != tt.want {
|
|
t.Errorf("hasBrazilTag(%v) = %v, want %v", tt.tags, got, tt.want)
|
|
}
|
|
}
|
|
}
|