munin/internal/search/registry_test.go

208 lines
5.9 KiB
Go

package search
import (
"context"
"path/filepath"
"testing"
"time"
"munin/internal/store"
)
// aufbauRegistry legt eine Datenbank mit einem Projekt und eine Registry mit
// bereits aufgebautem Index an.
func aufbauRegistry(t *testing.T) (*Registry, *store.Store, store.Project, *Index) {
t.Helper()
db, err := store.Open(filepath.Join(t.TempDir(), "test.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
ctx := context.Background()
projekt, err := db.CreateProject(ctx, store.NewID(), "test", "Test", "")
if err != nil {
t.Fatal(err)
}
r := NewRegistry(db)
// Den Index vorher anlegen — sonst sind Aktualisierungen ein No-op.
idx, err := r.For(ctx, projekt.ID)
if err != nil {
t.Fatal(err)
}
return r, db, projekt, idx
}
func schreibe(t *testing.T, db *store.Store, projekt store.Project, slug, inhalt string) store.Entry {
t.Helper()
e, err := db.PutEntry(context.Background(), store.EntryWrite{
ProjectID: projekt.ID, Slug: slug, Title: "Eintrag", Content: inhalt, Source: store.SourceAgent,
})
if err != nil {
t.Fatal(err)
}
return e
}
func findet(idx *Index, wort, slug string) bool {
for _, r := range idx.Search(wort, 10) {
if r.DocID == slug {
return true
}
}
return false
}
// TestUeberholteAktualisierungHaeltIndexAktuell stellt den Fall nach, dass sich
// zwei Schreiber zwischen Datenbank und Index überholen: Fassung 2 und 3 sind
// gespeichert, aber die Meldung für Fassung 3 kommt beim Index zuerst an.
//
// Unter Last lässt sich das kaum zuverlässig auslösen (siehe
// TestGleichzeitigesSchreibenHaeltSuchindexAktuell im Paket mcpserver), hier
// wird die Reihenfolge deshalb von Hand vertauscht.
func TestUeberholteAktualisierungHaeltIndexAktuell(t *testing.T) {
r, db, projekt, idx := aufbauRegistry(t)
schreibe(t, db, projekt, "umkaempft", "Stand wortzwei")
schreibe(t, db, projekt, "umkaempft", "Stand wortdrei")
r.Refresh(context.Background(), projekt.ID, "umkaempft") // Schreiber von Fassung 3
r.Refresh(context.Background(), projekt.ID, "umkaempft") // Schreiber von Fassung 2, verspätet
if !findet(idx, "wortdrei", "umkaempft") {
t.Error("der Index kennt den aktuellen Stand (Fassung 3) nicht")
}
if findet(idx, "wortzwei", "umkaempft") {
t.Error("der Index hält einen überholten Stand (Fassung 2)")
}
}
// TestVerspaeteteAktualisierungNachLoeschenBleibtWeg: Ein Eintrag wird
// geschrieben und gleich darauf gelöscht. Kommt die Meldung des Schreibers erst
// nach der des Löschens beim Index an, darf der Eintrag nicht wieder auftauchen.
func TestVerspaeteteAktualisierungNachLoeschenBleibtWeg(t *testing.T) {
r, db, projekt, idx := aufbauRegistry(t)
ctx := context.Background()
schreibe(t, db, projekt, "kurzlebig", "Stand wortgeloescht")
if err := db.MoveEntryToTrash(ctx, projekt.ID, "kurzlebig", "test"); err != nil {
t.Fatal(err)
}
r.Refresh(ctx, projekt.ID, "kurzlebig") // Löschender
r.Refresh(ctx, projekt.ID, "kurzlebig") // Schreiber, verspätet
if findet(idx, "wortgeloescht", "kurzlebig") {
t.Error("ein gelöschter Eintrag ist wieder im Suchindex")
}
}
// TestLangsamerAufbauBremstAndereProjekteNicht: Während der Index von Projekt A
// noch aufgebaut wird, muss Projekt B ohne Wartezeit durchkommen. Früher hielt
// der Aufbau eine Sperre für den ganzen Workspace.
func TestLangsamerAufbauBremstAndereProjekteNicht(t *testing.T) {
db, err := store.Open(filepath.Join(t.TempDir(), "test.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
ctx := context.Background()
gross, err := db.CreateProject(ctx, store.NewID(), "gross", "Groß", "")
if err != nil {
t.Fatal(err)
}
klein, err := db.CreateProject(ctx, store.NewID(), "klein", "Klein", "")
if err != nil {
t.Fatal(err)
}
r := NewRegistry(db)
imAufbau := make(chan struct{})
weiter := make(chan struct{})
r.nachDemLesen = func(projectID string) {
if projectID == gross.ID {
close(imAufbau)
<-weiter // Aufbau hängt, bis der Test ihn freigibt
}
}
fertig := make(chan error)
go func() {
_, err := r.For(ctx, gross.ID)
fertig <- err
}()
<-imAufbau
// Projekt B, während A noch aufgebaut wird.
klar := make(chan error)
go func() {
_, err := r.For(ctx, klein.ID)
klar <- err
}()
select {
case err := <-klar:
if err != nil {
t.Fatal(err)
}
case <-time.After(2 * time.Second):
t.Fatal("Projekt B wartet auf den Aufbau von Projekt A")
}
close(weiter)
if err := <-fertig; err != nil {
t.Fatal(err)
}
}
// TestAenderungWaehrendDesAufbausGehtNichtVerloren: Der Aufbau hat die
// Einträge schon gelesen, da wird einer davon geändert. Das Refresh des
// Schreibers muss warten und danach den neuen Stand nachtragen — sonst hielte
// der fertige Index den Stand von vor der Änderung.
func TestAenderungWaehrendDesAufbausGehtNichtVerloren(t *testing.T) {
db, err := store.Open(filepath.Join(t.TempDir(), "test.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { db.Close() })
ctx := context.Background()
projekt, err := db.CreateProject(ctx, store.NewID(), "test", "Test", "")
if err != nil {
t.Fatal(err)
}
schreibe(t, db, projekt, "eintrag", "Stand wortalt")
r := NewRegistry(db)
refreshFertig := make(chan struct{})
r.nachDemLesen = func(string) {
// Die Einträge sind gelesen, der Index noch nicht befüllt. Jetzt
// schreibt jemand und meldet es, wie es die Handler tun.
schreibe(t, db, projekt, "eintrag", "Stand wortneu")
go func() {
r.Refresh(ctx, projekt.ID, "eintrag")
close(refreshFertig)
}()
// Refresh darf nicht durchlaufen, solange der Aufbau läuft.
select {
case <-refreshFertig:
t.Error("Refresh lief während des Aufbaus durch")
case <-time.After(50 * time.Millisecond):
}
}
idx, err := r.For(ctx, projekt.ID)
if err != nil {
t.Fatal(err)
}
<-refreshFertig
if !findet(idx, "wortneu", "eintrag") {
t.Error("die Änderung während des Aufbaus fehlt im Index")
}
if findet(idx, "wortalt", "eintrag") {
t.Error("der Index hält den Stand von vor der Änderung")
}
}