package ingest
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"testing"
"craftdesign.group/quarry/internal/index"
)
// withFailingFrames puts a zit on PATH whose `frames` delivers the
// whole census and then fails, so a rebuild fails after its folds.
func withFailingFrames(t *testing.T) {
t.Helper()
real, err := exec.LookPath("zit")
if err != nil {
t.Skip("zit not on PATH")
}
dir := t.TempDir()
script := fmt.Sprintf("#!/bin/sh\nif [ \"$1\" = frames ]; then %q \"$@\"; echo 'frames: injected failure' >&2; exit 1; fi\nexec %q \"$@\"\n", real, real)
if err := os.WriteFile(filepath.Join(dir, "zit"), []byte(script), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
}
// The search index is external content over search_doc, and rows a
// rebuild folds while its triggers are off never reach it. A rebuild
// that failed after folding put the triggers back without rebuilding
// search, so the next update of such a row told the index to delete an
// entry it never held, and SQLite answered "database disk image is
// malformed".
func TestAFailedRebuildLeavesSearchConsistentWithItsRows(t *testing.T) {
p, ix, _ := testPipeline(t)
if _, err := p.Sync(); err != nil {
t.Fatal(err)
}
tl, _, err := ix.Timeline(index.TimelineFilter{Limit: 1, Kind: "ci"})
if err != nil || len(tl) == 0 {
t.Fatalf("no check-in to begin with: %v", err)
}
withFailingFrames(t)
if _, err := p.Reindex(); err == nil {
t.Fatal("a rebuild with a failing census read reported success")
}
c := tl[0]
if err := ix.FoldCheckins([]index.CheckinRecord{{Hash: c.Hash, Date: c.Date, User: c.User, Comment: "zanzibarite reworded"}}); err != nil {
t.Fatalf("FAIL: updating a row folded by the failed rebuild: %v", err)
}
rs, err := ix.Search("zanzibarite", "", 10)
if err != nil {
t.Fatalf("FAIL: search after the update: %v", err)
}
if len(rs) != 1 {
t.Errorf("FAIL: the updated row is not findable: %+v", rs)
} else {
t.Log("ok: a row folded by a failed rebuild updates cleanly and is findable")
}
}