package browse import ( "context" "fmt" "os" "os/exec" "path/filepath" "strings" "testing" "github.com/dolthub/dolt/go/libraries/doltcore/doltdb" "github.com/dolthub/dolt/go/libraries/utils/earl" "github.com/dolthub/dolt/go/libraries/utils/filesys" "github.com/dolthub/dolt/go/store/types" ) // The browse tests read a real bare NBS store built the way production stores // are: doltdb.WriteEmptyRepo creates the bare remote, then a working clone is // grown with the dolt CLI and pushed back over a file:// remote. That push // path produces exactly our production bare-store shape, and building the // fixture through the CLI keeps this package's tests independent of the // version-fragile editor/prolly write APIs (the same fragility this package is // meant to contain on the read side). Programmatic table building via the // doltdb editor APIs was considered and rejected as disproportionately // complex and brittle for a read-only browse layer. // // The fixture is built once in TestMain and shared read-only across tests. // When the dolt CLI is absent the tests skip rather than fail. const doltBin = "/opt/homebrew/bin/dolt" // Commit messages, oldest to newest on main. const ( msgInitial = "Initialize data repository" // C0, from WriteEmptyRepo, no parent msgAddUser = "add users" // C1: create empty users table msgInsert = "insert users" // C2: insert 3 rows msgModify = "modify users" // C3: update 1 row, insert 1 row msgAddItem = "add items" // C4: create empty items + docs tables ) // longDocBody is a >4KB longtext value; at this size Dolt stores the string // out-of-line (address-encoded) rather than inline, exercising the browse // layer's address-resolution path (a short string would stay inline and never // hit it). var longDocBody = strings.Repeat("The quick brown fox. ", 300) // 6300 bytes var ( haveDolt bool fixtureStore string // path to the bare store, main head = C4 hashByMessage = map[string]string{} ) func TestMain(m *testing.M) { os.Exit(runWithFixture(m)) } // runWithFixture builds the fixture (if dolt is available), runs the tests, and // cleans up. Split out so its defer runs before os.Exit. func runWithFixture(m *testing.M) int { if _, err := os.Stat(doltBin); err != nil { haveDolt = false return m.Run() } haveDolt = true dir, err := os.MkdirTemp("", "browse-fixture-") if err != nil { fmt.Fprintln(os.Stderr, "browse fixture: mkdtemp:", err) return 1 } defer os.RemoveAll(dir) store, err := buildFixture(dir) if err != nil { fmt.Fprintln(os.Stderr, "browse fixture build failed:", err) return 1 } fixtureStore = store if err := indexCommits(store); err != nil { fmt.Fprintln(os.Stderr, "browse fixture index failed:", err) return 1 } return m.Run() } // buildFixture creates the bare store and grows it via the CLI, returning the // bare store path. func buildFixture(root string) (string, error) { ctx := context.Background() bare := filepath.Join(root, "bare") if err := os.MkdirAll(bare, 0o755); err != nil { return "", err } url := earl.FileUrlFromPath(bare, os.PathSeparator) ddb, err := doltdb.LoadDoltDB(ctx, types.Format_DOLT, url, filesys.LocalFS) if err != nil { return "", fmt.Errorf("load bare: %w", err) } if err := ddb.WriteEmptyRepo(ctx, "main", "Fixture Owner", "owner@fixture.test"); err != nil { return "", fmt.Errorf("write empty repo: %w", err) } // Safe to close immediately after WriteEmptyRepo (novel tables only); the // generational-close panic documented in open.go only bites on reopen. if err := ddb.Close(); err != nil { return "", fmt.Errorf("close bare: %w", err) } home := filepath.Join(root, "home") if err := os.MkdirAll(home, 0o755); err != nil { return "", err } env := append(os.Environ(), "HOME="+home) run := func(dir string, args ...string) error { cmd := exec.Command(doltBin, args...) cmd.Dir = dir cmd.Env = env if out, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("dolt %s: %w\n%s", strings.Join(args, " "), err, out) } return nil } steps := [][]string{ {"config", "--global", "--add", "user.email", "cli@fixture.test"}, {"config", "--global", "--add", "user.name", "CLI Fixture"}, } for _, s := range steps { if err := run(root, s...); err != nil { return "", err } } work := filepath.Join(root, "work") if err := run(root, "clone", "file://"+filepath.ToSlash(bare), work); err != nil { return "", err } workSteps := [][]string{ {"sql", "-q", "create table users (id int primary key, name varchar(64))"}, {"commit", "-Am", msgAddUser}, {"sql", "-q", "insert into users values (1,'alice'),(2,'bob'),(3,'carol')"}, {"commit", "-Am", msgInsert}, {"branch", "dev"}, // dev head = C2 {"sql", "-q", "update users set name='alicia' where id=1; insert into users values (4,'dave')"}, {"commit", "-Am", msgModify}, // C4 adds the empty items table and, in the same commit, a docs table // carrying one >4KB longtext row — the out-of-line-text fixture — plus a // nulls table mixing real NULLs with the stored text "NULL". Folding them // into C4 keeps the commit topology (and every commit-count assertion) // unchanged; TestTables looks tables up by name, so the extra tables are // invisible to it while TestRowsResolvesLongText reads docs and the // null-mask tests read nulls directly. {"sql", "-q", "create table items (sku varchar(16) primary key, qty int)"}, {"sql", "-q", "create table docs (id int primary key, body longtext)"}, {"sql", "-q", fmt.Sprintf("insert into docs values (1, '%s')", longDocBody)}, {"sql", "-q", "create table nulls (id int primary key, note varchar(64))"}, // id=2 has no value at all; id=3 stores the four characters N,U,L,L. // Both render as "NULL", so only the mask can tell them apart. Six rows // so a page can start in the middle and still be a full page. {"sql", "-q", "insert into nulls values " + "(1,'alpha'),(2,NULL),(3,'NULL'),(4,'delta'),(5,NULL),(6,'zeta')"}, {"commit", "-Am", msgAddItem}, {"push", "origin", "main"}, {"push", "origin", "dev"}, } for _, s := range workSteps { if err := run(work, s...); err != nil { return "", err } } return bare, nil } // indexCommits records the hash of each commit on main by its message. func indexCommits(store string) error { ctx := context.Background() db, err := Open(ctx, store) if err != nil { return err } defer db.Close() commits, _, err := db.Log(ctx, "main", "", 100) if err != nil { return err } for _, c := range commits { hashByMessage[c.Message] = c.Hash } return nil } // requireFixture skips the test when the dolt CLI (and thus the fixture) is // unavailable. func requireFixture(t *testing.T) { t.Helper() if !haveDolt { t.Skipf("dolt CLI not found at %s; skipping browse tests", doltBin) } } // openFixture opens the shared fixture store for one test. func openFixture(t *testing.T) *DB { t.Helper() requireFixture(t) db, err := Open(context.Background(), fixtureStore) if err != nil { t.Fatalf("open fixture: %v", err) } t.Cleanup(func() { db.Close() }) return db } // commitHash returns the recorded hash for a commit message. func commitHash(t *testing.T, message string) string { t.Helper() h, ok := hashByMessage[message] if !ok { t.Fatalf("no recorded commit for message %q", message) } return h }