package dict_test
import (
"encoding/binary"
"io"
"os"
"path/filepath"
"slices"
"strings"
"testing"
"chirami/internal/dict"
"chirami/internal/dict/build"
)
func buildTestDict(t *testing.T, includeNames bool) *dict.Dict {
t.Helper()
jmdictFile, err := os.Open("testdata/jmdict.json")
if err != nil {
t.Fatalf("open jmdict.json: %v", err)
}
defer jmdictFile.Close()
kanjidicFile, err := os.Open("testdata/kanjidic.json")
if err != nil {
t.Fatalf("open kanjidic.json: %v", err)
}
defer kanjidicFile.Close()
accentsFile, err := os.Open("testdata/accents.txt")
if err != nil {
t.Fatalf("open accents.txt: %v", err)
}
defer accentsFile.Close()
xmlFile, err := os.Open("testdata/jmdict.xml")
if err != nil {
t.Fatalf("open jmdict.xml: %v", err)
}
defer xmlFile.Close()
furiFile, err := os.Open("testdata/furigana.json")
if err != nil {
t.Fatalf("open furigana.json: %v", err)
}
defer furiFile.Close()
n5File, err := os.Open("testdata/jlpt_n5.csv")
if err != nil {
t.Fatalf("open jlpt_n5: %v", err)
}
defer n5File.Close()
n4File, err := os.Open("testdata/jlpt_n4.csv")
if err != nil {
t.Fatalf("open jlpt_n4: %v", err)
}
defer n4File.Close()
n1File, err := os.Open("testdata/jlpt_n1.csv")
if err != nil {
t.Fatalf("open jlpt_n1: %v", err)
}
defer n1File.Close()
opts := []build.BuildOption{
build.WithPriorities(xmlFile),
build.WithFurigana(furiFile),
build.WithJLPT(map[string]io.Reader{
"N5": n5File,
"N4": n4File,
"N1": n1File,
}),
}
kradFile, err := os.Open("testdata/kradfile.json")
if err == nil {
defer kradFile.Close()
opts = append(opts, build.WithKradfile(kradFile))
}
radkFile, err := os.Open("testdata/radkfile.json")
if err == nil {
defer radkFile.Close()
opts = append(opts, build.WithRadkfile(radkFile))
}
if includeNames {
namesFile, err := os.Open("testdata/jmnedict.json")
if err != nil {
t.Fatalf("open jmnedict.json: %v", err)
}
defer namesFile.Close()
opts = append(opts, build.WithNames(namesFile))
}
outPath := filepath.Join(t.TempDir(), "testdict.bin")
err = build.Build(jmdictFile, kanjidicFile, accentsFile, outPath, opts...)
if err != nil {
t.Fatalf("build test dict: %v", err)
}
d, err := dict.Open(outPath)
if err != nil {
t.Fatalf("open test dict: %v", err)
}
t.Cleanup(func() {
d.Close()
})
return d
}
func TestLookup(t *testing.T) {
d := buildTestDict(t, false)
// 1. Kanji lookup
taberuKanji := d.Lookup("食べる")
if len(taberuKanji) == 0 {
t.Fatalf("Lookup('食べる') returned 0 entries")
}
if taberuKanji[0].Kanji[0].Text != "食べる" {
t.Errorf("got %q, want '食べる'", taberuKanji[0].Kanji[0].Text)
}
// 2. Hiragana lookup matching kanji form's reading
taberuHira := d.Lookup("たべる")
if len(taberuHira) == 0 {
t.Fatalf("Lookup('たべる') returned 0 entries")
}
if taberuHira[0].ID != taberuKanji[0].ID {
t.Errorf("Lookup('たべる') ID %d != Lookup('食べる') ID %d", taberuHira[0].ID, taberuKanji[0].ID)
}
// 3. Katakana lookup matching hiragana reading
taberuKata := d.Lookup("タベル")
if len(taberuKata) == 0 {
t.Fatalf("Lookup('タベル') returned 0 entries")
}
if taberuKata[0].ID != taberuKanji[0].ID {
t.Errorf("Lookup('タベル') ID %d != Lookup('食べる') ID %d", taberuKata[0].ID, taberuKanji[0].ID)
}
// 4. Katakana-only entry
ramen := d.Lookup("ラーメン")
if len(ramen) == 0 {
t.Fatalf("Lookup('ラーメン') returned 0 entries")
}
if ramen[0].Kana[0].Text != "ラーメン" {
t.Errorf("got %q, want 'ラーメン'", ramen[0].Kana[0].Text)
}
// Hiragana matching katakana entry
ramenHira := d.Lookup("らーめん")
if len(ramenHira) == 0 {
t.Fatalf("Lookup('らーめん') returned 0 entries")
}
if ramenHira[0].ID != ramen[0].ID {
t.Errorf("Lookup('らーめん') ID %d != Lookup('ラーメン') ID %d", ramenHira[0].ID, ramen[0].ID)
}
// 5. Kana-only entry
kasakasa := d.Lookup("かさかさ")
if len(kasakasa) == 0 {
t.Fatalf("Lookup('かさかさ') returned 0 entries")
}
// 6. Particle は
ha := d.Lookup("は")
if len(ha) == 0 {
t.Fatalf("Lookup('は') returned 0 entries")
}
// 7. Non-existent entry
nonExistent := d.Lookup("存在しない単語xyz")
if len(nonExistent) != 0 {
t.Errorf("expected 0 entries for non-existent word, got %d", len(nonExistent))
}
}
func TestLookupPriorityRanking(t *testing.T) {
d := buildTestDict(t, false)
// "きょう": 今日 (ID 1579110, nf02) must beat 京 (ID 1233320, nf03)
entries := d.Lookup("きょう")
if len(entries) < 2 {
t.Fatalf("expected at least 2 entries for 'きょう', got %d", len(entries))
}
if entries[0].Kanji[0].Text != "今日" {
t.Fatalf("first entry for 'きょう' is %q (ID %d), want '今日' (ID 1579110)", entries[0].Kanji[0].Text, entries[0].ID)
}
if entries[1].Kanji[0].Text != "京" {
t.Errorf("second entry for 'きょう' is %q, want '京'", entries[1].Kanji[0].Text)
}
}
func TestJLPT(t *testing.T) {
d := buildTestDict(t, false)
// 今日 listed in both N5 and N4 -> N5 wins (easiest level)
kyo := d.Lookup("今日")
if len(kyo) == 0 {
t.Fatalf("Lookup('今日') empty")
}
if kyo[0].JLPT != "N5" {
t.Errorf("今日 JLPT = %q, want 'N5'", kyo[0].JLPT)
}
// 食べる in N5
taberu := d.Lookup("食べる")
if len(taberu) == 0 {
t.Fatalf("Lookup('食べる') empty")
}
if taberu[0].JLPT != "N5" {
t.Errorf("食べる JLPT = %q, want 'N5'", taberu[0].JLPT)
}
// 勉強 in N4
benkyo := d.Lookup("勉強")
if len(benkyo) == 0 {
t.Fatalf("Lookup('勉強') empty")
}
if benkyo[0].JLPT != "N4" {
t.Errorf("勉強 JLPT = %q, want 'N4'", benkyo[0].JLPT)
}
// 日本語 in N1
nihongo := d.Lookup("日本語")
if len(nihongo) == 0 {
t.Fatalf("Lookup('日本語') empty")
}
if nihongo[0].JLPT != "N1" {
t.Errorf("日本語 JLPT = %q, want 'N1'", nihongo[0].JLPT)
}
}
func TestNames(t *testing.T) {
d := buildTestDict(t, true)
// "きょう" has JMdict entries for 今日 and 京, and JMnedict name entry for surname "今日" (Kyou)
entries := d.Lookup("きょう")
if len(entries) < 3 {
t.Fatalf("expected at least 3 entries, got %d", len(entries))
}
// The first 2 entries must be JMdict entries (ID < 100000000)
if entries[0].ID >= 100000000 || entries[1].ID >= 100000000 {
t.Errorf("first entries should be JMdict, got ID %d and %d", entries[0].ID, entries[1].ID)
}
// The name entry must be ranked after all JMdict entries
var nameEntry *dict.Entry
for i := range entries {
if entries[i].ID >= 100000000 {
nameEntry = &entries[i]
break
}
}
if nameEntry == nil {
t.Fatalf("no name entry found in Lookup('きょう')")
}
if !slices.Contains(nameEntry.Senses[0].Misc, "surname") {
t.Errorf("name sense misc = %v, want 'surname'", nameEntry.Senses[0].Misc)
}
if !slices.Contains(nameEntry.Senses[0].Gloss, "Kyou") {
t.Errorf("name sense gloss = %v, want 'Kyou'", nameEntry.Senses[0].Gloss)
}
}
func TestLookupReading(t *testing.T) {
d := buildTestDict(t, false)
kyoEntries := d.LookupReading("今日", "きょう")
if len(kyoEntries) < 2 {
t.Fatalf("expected at least 2 entries for '今日', got %d", len(kyoEntries))
}
if kyoEntries[0].Kana[0].Text != "きょう" {
t.Errorf("first entry reading = %q, want 'きょう'", kyoEntries[0].Kana[0].Text)
}
konnichiEntries := d.LookupReading("今日", "こんにち")
if len(konnichiEntries) < 2 {
t.Fatalf("expected at least 2 entries for '今日', got %d", len(konnichiEntries))
}
if konnichiEntries[0].Kana[0].Text != "こんにち" {
t.Errorf("first entry reading = %q, want 'こんにち'", konnichiEntries[0].Kana[0].Text)
}
// Kana-only lookup ranks the particle は ahead of kanji homophone 葉 (nf03).
haEntries := d.LookupReading("は", "は")
if len(haEntries) < 2 {
t.Fatalf("expected at least 2 entries for 'は', got %d", len(haEntries))
}
if haEntries[0].ID != 2028920 {
t.Errorf("first entry for 'は' = ID %d (%v), want 2028920 (particle は)", haEntries[0].ID, haEntries[0].Kanji)
}
}
func TestLongestPrefix(t *testing.T) {
d := buildTestDict(t, false)
n, entries := d.LongestPrefix("日本語を勉強", 8)
expectedLen := len("日本語")
if n != expectedLen {
t.Errorf("LongestPrefix byte length = %d, want %d", n, expectedLen)
}
if len(entries) == 0 {
t.Fatalf("LongestPrefix returned 0 entries")
}
if entries[0].Kanji[0].Text != "日本語" {
t.Errorf("got %q, want '日本語'", entries[0].Kanji[0].Text)
}
n, entries = d.LongestPrefix("あいうえお", 3)
if n != 0 || entries != nil {
t.Errorf("expected 0, nil for no match, got %d, %v", n, entries)
}
}
func TestKanji(t *testing.T) {
d := buildTestDict(t, false)
k, ok := d.Kanji('食')
if !ok {
t.Fatalf("Kanji('食') not found")
}
if k.Literal != "食" {
t.Errorf("Literal = %q, want '食'", k.Literal)
}
if k.Strokes != 9 {
t.Errorf("Strokes = %d, want 9", k.Strokes)
}
if k.Grade != 2 {
t.Errorf("Grade = %d, want 2", k.Grade)
}
if !slices.Contains(k.Meanings, "eat") {
t.Errorf("Meanings %v does not contain 'eat'", k.Meanings)
}
if !slices.Contains(k.On, "ショク") {
t.Errorf("On %v does not contain 'ショク'", k.On)
}
if !slices.Contains(k.Kun, "た.べる") {
t.Errorf("Kun %v does not contain 'た.べる'", k.Kun)
}
k2, ok := d.Kanji('語')
if !ok {
t.Fatalf("Kanji('語') not found")
}
if k2.Strokes != 14 {
t.Errorf("Strokes = %d, want 14", k2.Strokes)
}
_, ok = d.Kanji('?')
if ok {
t.Errorf("expected Kanji('?') to return false")
}
}
func TestPitchAttachment(t *testing.T) {
d := buildTestDict(t, false)
// 1. 食べる -> downstep 2
entries := d.Lookup("食べる")
if len(entries) == 0 {
t.Fatalf("no entry for 食べる")
}
e := entries[0]
if len(e.Pitch) == 0 {
t.Fatalf("no pitch attached to 食べる")
}
if e.Pitch[0].Reading != "たべる" {
t.Errorf("pitch reading = %q, want 'たべる'", e.Pitch[0].Reading)
}
if !slices.Equal(e.Pitch[0].Downsteps, []int{2}) {
t.Errorf("pitch downsteps = %v, want [2]", e.Pitch[0].Downsteps)
}
// 2. かさかさ -> downsteps 1, 0
entries = d.Lookup("かさかさ")
if len(entries) == 0 {
t.Fatalf("no entry for かさかさ")
}
e = entries[0]
if len(e.Pitch) == 0 {
t.Fatalf("no pitch attached to かさかさ")
}
if !slices.Equal(e.Pitch[0].Downsteps, []int{1, 0}) {
t.Errorf("pitch downsteps = %v, want [1, 0]", e.Pitch[0].Downsteps)
}
// 3. ラーメン -> downstep 1
entries = d.Lookup("ラーメン")
if len(entries) == 0 {
t.Fatalf("no entry for ラーメン")
}
e = entries[0]
if len(e.Pitch) == 0 {
t.Fatalf("no pitch attached to ラーメン")
}
if !slices.Equal(e.Pitch[0].Downsteps, []int{1}) {
t.Errorf("pitch downsteps = %v, want [1]", e.Pitch[0].Downsteps)
}
}
func TestTagLabel(t *testing.T) {
d := buildTestDict(t, false)
if label := d.TagLabel("v1"); label != "Ichidan verb" {
t.Errorf("d.TagLabel('v1') = %q, want 'Ichidan verb'", label)
}
if label := d.TagLabel("vt"); label != "transitive verb" {
t.Errorf("d.TagLabel('vt') = %q, want 'transitive verb'", label)
}
if label := d.TagLabel("n"); label != "noun (common) (futsuumeishi)" {
t.Errorf("d.TagLabel('n') = %q, want 'noun (common) (futsuumeishi)'", label)
}
if label := d.TagLabel("unknown_tag_xyz"); label != "unknown_tag_xyz" {
t.Errorf("d.TagLabel('unknown_tag_xyz') = %q, want 'unknown_tag_xyz'", label)
}
}
func TestFurigana(t *testing.T) {
d := buildTestDict(t, false)
rubies, ok := d.Furigana("日本語", "にほんご")
if !ok {
t.Fatalf("Furigana('日本語', 'にほんご') not found")
}
want := []dict.Ruby{
{Text: "日", Ruby: "に"},
{Text: "本", Ruby: "ほん"},
{Text: "語", Ruby: "ご"},
}
if !slices.Equal(rubies, want) {
t.Errorf("Furigana('日本語', 'にほんご') = %+v, want %+v", rubies, want)
}
rubies, ok = d.Furigana("今日", "きょう")
if !ok {
t.Fatalf("Furigana('今日', 'きょう') not found")
}
wantKyo := []dict.Ruby{
{Text: "今日", Ruby: "きょう"},
}
if !slices.Equal(rubies, wantKyo) {
t.Errorf("Furigana('今日', 'きょう') = %+v, want %+v", rubies, wantKyo)
}
rubies, ok = d.Furigana("食べる", "たべる")
if !ok {
t.Fatalf("Furigana('食べる', 'たべる') not found")
}
wantTaberu := []dict.Ruby{
{Text: "食", Ruby: "た"},
{Text: "べる", Ruby: ""},
}
if !slices.Equal(rubies, wantTaberu) {
t.Errorf("Furigana('食べる', 'たべる') = %+v, want %+v", rubies, wantTaberu)
}
rubies, ok = d.Furigana("大人", "おとな")
if !ok {
t.Fatalf("Furigana('大人', 'おとな') not found")
}
wantOtona := []dict.Ruby{
{Text: "大人", Ruby: "おとな"},
}
if !slices.Equal(rubies, wantOtona) {
t.Errorf("Furigana('大人', 'おとな') = %+v, want %+v", rubies, wantOtona)
}
rubies, ok = d.Furigana("食べる", "タベル")
if !ok {
t.Fatalf("Furigana('食べる', 'タベル') not found")
}
if !slices.Equal(rubies, wantTaberu) {
t.Errorf("Furigana('食べる', 'タベル') = %+v, want %+v", rubies, wantTaberu)
}
_, ok = d.Furigana("存在しない", "そんざいしない")
if ok {
t.Errorf("expected false for non-existent furigana")
}
}
func TestComponents(t *testing.T) {
d := buildTestDict(t, false)
comps := d.Components('食')
if !slices.Equal(comps, []rune{'食'}) {
t.Errorf("Components('食') = %v, want ['食']", comps)
}
comps = d.Components('語')
want := []rune{'言', '口', '五'}
if !slices.Equal(comps, want) {
t.Errorf("Components('語') = %v, want %v", comps, want)
}
comps = d.Components('良')
if !slices.Equal(comps, []rune{'艮'}) {
t.Errorf("Components('良') = %v, want ['艮']", comps)
}
// Verify display component forms: 化->⺅, 艾->艹, 汁->氵, 込->辶, |->丨, ノ->丿
if comps = d.Components('休'); !slices.Equal(comps, []rune{'⺅', '木'}) {
t.Errorf("Components('休') = %v (%c), want [⺅, 木]", comps, comps)
}
if comps = d.Components('花'); !slices.Equal(comps, []rune{'⺅', '匕', '艹'}) {
t.Errorf("Components('花') = %v (%c), want [⺅, 匕, 艹]", comps, comps)
}
if comps = d.Components('働'); !slices.Equal(comps, []rune{'丨', '一', '⺅', '力', '日', '丿'}) {
t.Errorf("Components('働') = %v (%c), want [丨, 一, ⺅, 力, 日, 丿]", comps, comps)
}
if comps = d.Components('?'); comps != nil {
t.Errorf("Components('?') = %v, want nil", comps)
}
}
func TestKanjiWith(t *testing.T) {
d := buildTestDict(t, false)
// In testdata/radkfile.json, 艮 has ["良", "銀", "限"]
// In testdata/kanjidic.json:
// 銀: freq 395
// 限: freq 405
// 良: freq 501
// Order must be: 銀, 限, 良
res := d.KanjiWith('艮', 10)
want := []rune{'銀', '限', '良'}
if !slices.Equal(res, want) {
t.Errorf("KanjiWith('艮', 10) = %v (%c %c %c), want %v (%c %c %c)",
res, res[0], res[1], res[2], want, want[0], want[1], want[2])
}
// KanjiWith('良', 8) resolves 良 -> single component 艮 -> returns [銀, 限, 良]
res = d.KanjiWith('良', 8)
if !slices.Equal(res, want) {
t.Errorf("KanjiWith('良', 8) = %v, want %v", res, want)
}
// Both display form and placeholder form must work:
// ⺅ and 化
k1 := d.KanjiWith('⺅', 10)
k2 := d.KanjiWith('化', 10)
if len(k1) == 0 || !slices.Equal(k1, k2) {
t.Errorf("KanjiWith('⺅') != KanjiWith('化'): %v vs %v", k1, k2)
}
// 艹 and 艾
if k1, k2 := d.KanjiWith('艹', 10), d.KanjiWith('艾', 10); len(k1) == 0 || !slices.Equal(k1, k2) {
t.Errorf("KanjiWith('艹') != KanjiWith('艾'): %v vs %v", k1, k2)
}
// 氵 and 汁
if k1, k2 := d.KanjiWith('氵', 10), d.KanjiWith('汁', 10); len(k1) == 0 || !slices.Equal(k1, k2) {
t.Errorf("KanjiWith('氵') != KanjiWith('汁'): %v vs %v", k1, k2)
}
// 辶 and 込
if k1, k2 := d.KanjiWith('辶', 10), d.KanjiWith('込', 10); len(k1) == 0 || !slices.Equal(k1, k2) {
t.Errorf("KanjiWith('辶') != KanjiWith('込'): %v vs %v", k1, k2)
}
// 丨 and |
if k1, k2 := d.KanjiWith('丨', 10), d.KanjiWith('|', 10); len(k1) == 0 || !slices.Equal(k1, k2) {
t.Errorf("KanjiWith('丨') != KanjiWith('|'): %v vs %v", k1, k2)
}
// 丿 and ノ
if k1, k2 := d.KanjiWith('丿', 10), d.KanjiWith('ノ', 10); len(k1) == 0 || !slices.Equal(k1, k2) {
t.Errorf("KanjiWith('丿') != KanjiWith('ノ'): %v vs %v", k1, k2)
}
// Max limit
res = d.KanjiWith('艮', 2)
if !slices.Equal(res, []rune{'銀', '限'}) {
t.Errorf("KanjiWith('艮', 2) = %v, want ['銀', '限']", res)
}
// Non-positive max returns nil
if res = d.KanjiWith('艮', 0); res != nil {
t.Errorf("KanjiWith('艮', 0) = %v, want nil", res)
}
if res = d.KanjiWith('艮', -1); res != nil {
t.Errorf("KanjiWith('艮', -1) = %v, want nil", res)
}
}
func TestComponentName(t *testing.T) {
d := buildTestDict(t, false)
cases := []struct {
r rune
want string
}{
{'宀', "roof"},
{'亠', "lid"},
{'冖', "cover"},
{'儿', "legs"},
{'丨', "line"},
{'丿', "slash"},
{'匕', "spoon"},
{'艹', "grass"},
{'⺅', "person"},
{'忄', "heart"},
{'氵', "water"},
{'辶', "walk"},
{'食', "eat"},
{'⺡', "water"},
{'⺉', "knife"},
}
for _, tc := range cases {
got := d.ComponentName(tc.r)
if got != tc.want {
t.Errorf("ComponentName(%c / U+%04X) = %q, want %q", tc.r, tc.r, got, tc.want)
}
}
}
func TestKanjiComponentsAndNamesTable(t *testing.T) {
d := buildTestDict(t, false)
table := []struct {
kanji rune
comps []rune
names []string
}{
{'休', []rune{'⺅', '木'}, []string{"person", "tree"}},
{'花', []rune{'⺅', '匕', '艹'}, []string{"person", "spoon", "grass"}},
{'働', []rune{'丨', '一', '⺅', '力', '日', '丿'}, []string{"line", "one", "person", "power", "sun", "slash"}},
{'語', []rune{'言', '口', '五'}, []string{"words", "mouth", "five"}},
{'読', []rune{'言', '士', '儿', '冖'}, []string{"words", "scholar", "legs", "cover"}},
{'海', []rune{'氵', '母', '毋', '乞'}, []string{"water", "mother", "do not", "beg"}},
{'道', []rune{'辶', '自', '丷', '首'}, []string{"walk", "oneself", "horns", "head"}},
{'家', []rune{'宀', '豕'}, []string{"roof", "pig"}},
{'想', []rune{'心', '木', '目'}, []string{"heart", "tree", "eye"}},
{'校', []rune{'父', '木', '亠'}, []string{"father", "tree", "lid"}},
}
for _, tc := range table {
gotComps := d.Components(tc.kanji)
if !slices.Equal(gotComps, tc.comps) {
t.Errorf("Components(%c) = %v (%c), want %v (%c)", tc.kanji, gotComps, gotComps, tc.comps, tc.comps)
}
gotNames := make([]string, len(gotComps))
for i, c := range gotComps {
gotNames[i] = d.ComponentName(c)
}
if !slices.Equal(gotNames, tc.names) {
t.Errorf("ComponentNames for %c = %v, want %v", tc.kanji, gotNames, tc.names)
}
}
}
func TestWordsWith(t *testing.T) {
d := buildTestDict(t, false)
words := d.WordsWith('食', 10)
if len(words) < 2 {
t.Fatalf("WordsWith('食', 10) returned %d entries, want at least 2", len(words))
}
// First should be 食べる (common, priority nf02)
if words[0].Kanji[0].Text != "食べる" {
t.Errorf("words[0] = %q, want '食べる'", words[0].Kanji[0].Text)
}
// Second should be 食事
if words[1].Kanji[0].Text != "食事" {
t.Errorf("words[1] = %q, want '食事'", words[1].Kanji[0].Text)
}
// Verify that 食偽 (search-only sK form) was excluded
for _, w := range words {
for _, k := range w.Kanji {
if k.Text == "食偽" {
t.Errorf("WordsWith('食') included search-only word '食偽'")
}
}
}
// Max limit
wordsLimited := d.WordsWith('食', 1)
if len(wordsLimited) != 1 {
t.Errorf("WordsWith('食', 1) length = %d, want 1", len(wordsLimited))
}
// Non-positive max returns nil
if w := d.WordsWith('食', 0); w != nil {
t.Errorf("WordsWith('食', 0) = %v, want nil", w)
}
// Non-existent kanji
if w := d.WordsWith('?', 10); w != nil {
t.Errorf("WordsWith('?', 10) = %v, want nil", w)
}
}
func TestOpenRejectsOldVersion(t *testing.T) {
// A file with version 1
var buf [160]byte
copy(buf[0:8], dict.MagicString)
binary.LittleEndian.PutUint32(buf[8:12], 1) // version 1
tmpPath := filepath.Join(t.TempDir(), "olddict.bin")
if err := os.WriteFile(tmpPath, buf[:], 0644); err != nil {
t.Fatal(err)
}
_, err := dict.Open(tmpPath)
if err == nil {
t.Fatal("expected error opening old version file, got nil")
}
if !strings.Contains(err.Error(), "run chirami setup") {
t.Errorf("error %q does not contain 'run chirami setup'", err.Error())
}
}