package store import ( "reflect" "testing" ) func rr(book, base, reading string, first, occ int) RubyReading { return RubyReading{BookID: book, Base: base, Reading: reading, FirstChapter: first, Occurrences: occ} } func TestRubyReadingsReplaceAndRead(t *testing.T) { s, _ := openTemp(t) if err := s.ReplaceRubyReadings("b", []RubyReading{ rr("b", "漢字", "かんじ", 2, 5), rr("b", "東京", "とうきょう", 1, 3), }); err != nil { t.Fatal(err) } if err := s.ReplaceRubyReadings("other", []RubyReading{rr("other", "京", "きょう", 1, 1)}); err != nil { t.Fatal(err) } got, err := s.RubyReadingsForBook("b") if err != nil { t.Fatal(err) } // Ordered by (first_chapter, base, reading): 東京(ch1) before 漢字(ch2). want := []RubyReading{ rr("b", "東京", "とうきょう", 1, 3), rr("b", "漢字", "かんじ", 2, 5), } if !reflect.DeepEqual(got, want) { t.Fatalf("read = %#v, want %#v (other book must not leak)", got, want) } } func TestRubyReadingsReplaceIsFullAndIdempotent(t *testing.T) { s, _ := openTemp(t) first := []RubyReading{rr("b", "漢字", "かんじ", 3, 4), rr("b", "海", "うみ", 1, 2)} if err := s.ReplaceRubyReadings("b", first); err != nil { t.Fatal(err) } // Re-ingesting the identical aggregate is a no-op (idempotent). if err := s.ReplaceRubyReadings("b", first); err != nil { t.Fatal(err) } got, _ := s.RubyReadingsForBook("b") if len(got) != 2 { t.Fatalf("identical re-replace must not add rows, got %#v", got) } // A source edit re-ingest: 海 removed entirely, 漢字 moved to ch 1 with a new count. // The full replace must DROP the phantom 海 and CONVERGE 漢字 — a prior upsert-only // path would have left 海 forever (external-review #6). if err := s.ReplaceRubyReadings("b", []RubyReading{rr("b", "漢字", "かんじ", 1, 2)}); err != nil { t.Fatal(err) } got, _ = s.RubyReadingsForBook("b") want := []RubyReading{rr("b", "漢字", "かんじ", 1, 2)} if !reflect.DeepEqual(got, want) { t.Fatalf("full replace must drop the removed pair and converge, got %#v", got) } } func TestRubyReadingsVariantReadingsAreDistinctRows(t *testing.T) { // The same base with two readings (variant yomi) are two rows, not a collision. s, _ := openTemp(t) if err := s.ReplaceRubyReadings("b", []RubyReading{ rr("b", "海", "うみ", 1, 2), rr("b", "海", "かい", 4, 1), }); err != nil { t.Fatal(err) } got, err := s.RubyReadingsForBook("b") if err != nil { t.Fatal(err) } if len(got) != 2 { t.Fatalf("variant readings must be distinct rows, got %#v", got) } } func TestRubyReadingsReplaceEmptyClears(t *testing.T) { // A source that dropped all its furigana must clear the book's rows. s, _ := openTemp(t) if err := s.ReplaceRubyReadings("b", []RubyReading{rr("b", "漢字", "かんじ", 1, 1)}); err != nil { t.Fatal(err) } if err := s.ReplaceRubyReadings("b", nil); err != nil { t.Fatal(err) } got, _ := s.RubyReadingsForBook("b") if len(got) != 0 { t.Fatalf("empty replace must clear the book, got %#v", got) } }