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 table
)
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},
{"sql", "-q", "create table items (sku varchar(16) primary key, qty int)"},
{"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
}