234 lines
6.7 KiB
Go
234 lines
6.7 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")
|
|
}
|
|
}
|
|
|
|
// TestRegistryBevorzugtKuratierte prüft, dass die Registry kuratierten
|
|
// Einträgen den Suchfaktor mitgibt — beim Aufbau wie beim Nachziehen.
|
|
func TestRegistryBevorzugtKuratierte(t *testing.T) {
|
|
r, db, projekt, idx := aufbauRegistry(t)
|
|
ctx := context.Background()
|
|
|
|
for _, kuratiert := range []bool{false, true} {
|
|
slug := "ungeprueft"
|
|
if kuratiert {
|
|
slug = "geprueft"
|
|
}
|
|
if _, err := db.PutEntry(ctx, store.EntryWrite{
|
|
ProjectID: projekt.ID, Slug: slug, Title: "Deployment", Content: "Ablauf.",
|
|
Source: store.SourceHuman, Curated: kuratiert,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r.Refresh(ctx, projekt.ID, slug)
|
|
}
|
|
|
|
treffer := idx.Search("deployment", 10)
|
|
if len(treffer) != 2 || treffer[0].DocID != "geprueft" {
|
|
t.Errorf("Treffer = %v, erwartet den kuratierten zuerst", ergebnisIDs(treffer))
|
|
}
|
|
}
|