Skip to content

Commit e7d0d93

Browse files
steveyeggeclaude
andcommitted
fix(test): isolate regression tests from production Dolt server
Regression tests were connecting to the production Dolt server on port 3307, polluting it with test databases. Start a dedicated Dolt server on a dynamic port in TestMain and pass BEADS_DOLT_PORT to all bd subprocess invocations via runEnv(). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6219b86 commit e7d0d93

2 files changed

Lines changed: 148 additions & 3 deletions

File tree

tests/regression/discovery_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// on 2026-02-22. These tests exercise the candidate binary ONLY (not differential)
55
// since bd export was removed from main (BUG-1 in DISCOVERY.md).
66
//
7-
// These tests require a running Dolt server on 127.0.0.1:3307.
7+
// TestMain starts an isolated Dolt server on a dynamic port (via BEADS_DOLT_PORT).
88
// Each test uses a unique prefix to avoid cross-contamination (BUG-6).
99
package regression
1010

tests/regression/regression_test.go

Lines changed: 147 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,23 @@ package regression
1212
import (
1313
"archive/tar"
1414
"compress/gzip"
15+
"database/sql"
1516
"encoding/json"
1617
"fmt"
1718
"io"
19+
"net"
1820
"net/http"
1921
"os"
2022
"os/exec"
2123
"path/filepath"
2224
"runtime"
2325
"sort"
26+
"strconv"
2427
"strings"
2528
"testing"
2629
"time"
30+
31+
_ "github.com/go-sql-driver/mysql"
2732
)
2833

2934
// baselineBin is the path to the pinned baseline bd binary.
@@ -32,24 +37,33 @@ var baselineBin string
3237
// candidateBin is the path to the bd binary built from the current worktree.
3338
var candidateBin string
3439

40+
// testDoltServerPort is the port of the isolated Dolt server started by TestMain.
41+
var testDoltServerPort int
42+
3543
func TestMain(m *testing.M) {
3644
if runtime.GOOS == "windows" {
3745
fmt.Fprintln(os.Stderr, "regression tests not yet supported on Windows (zip extraction needed)")
3846
os.Exit(0)
3947
}
4048

49+
// Start an isolated Dolt server so regression tests don't pollute
50+
// the production database on port 3307.
51+
cleanupServer := startTestDoltServer()
52+
4153
tmpDir, err := os.MkdirTemp("", "bd-regression-bin-*")
4254
if err != nil {
4355
fmt.Fprintf(os.Stderr, "creating temp dir: %v\n", err)
56+
cleanupServer()
4457
os.Exit(1)
4558
}
46-
defer os.RemoveAll(tmpDir)
4759

4860
// Build candidate from current worktree
4961
candidateBin = filepath.Join(tmpDir, "bd-candidate")
5062
fmt.Fprintln(os.Stderr, "Building candidate binary...")
5163
if err := buildCandidate(candidateBin); err != nil {
5264
fmt.Fprintf(os.Stderr, "building candidate: %v\n", err)
65+
os.RemoveAll(tmpDir)
66+
cleanupServer()
5367
os.Exit(1)
5468
}
5569

@@ -58,11 +72,16 @@ func TestMain(m *testing.M) {
5872
baselineBin, err = getBaseline()
5973
if err != nil {
6074
fmt.Fprintf(os.Stderr, "getting baseline: %v\n", err)
75+
os.RemoveAll(tmpDir)
76+
cleanupServer()
6177
os.Exit(1)
6278
}
6379

6480
fmt.Fprintf(os.Stderr, "Baseline: %s\nCandidate: %s\n\n", baselineBin, candidateBin)
65-
os.Exit(m.Run())
81+
code := m.Run()
82+
os.RemoveAll(tmpDir)
83+
cleanupServer()
84+
os.Exit(code)
6685
}
6786

6887
// ---------------------------------------------------------------------------
@@ -242,6 +261,9 @@ func (w *workspace) runEnv() []string {
242261
"BEADS_NO_DAEMON=1",
243262
"GIT_CONFIG_NOSYSTEM=1",
244263
}
264+
if testDoltServerPort != 0 {
265+
env = append(env, "BEADS_DOLT_PORT="+strconv.Itoa(testDoltServerPort))
266+
}
245267
if v := os.Getenv("TMPDIR"); v != "" {
246268
env = append(env, "TMPDIR="+v)
247269
}
@@ -695,3 +717,126 @@ func (w *workspace) tryCreate(args ...string) (string, error) {
695717
w.createdIDs = append(w.createdIDs, id)
696718
return id, nil
697719
}
720+
721+
// ---------------------------------------------------------------------------
722+
// Test Dolt server (isolation from production)
723+
// ---------------------------------------------------------------------------
724+
725+
// startTestDoltServer starts a dedicated Dolt SQL server in a temp directory
726+
// on a dynamic port. This prevents regression tests from creating databases on
727+
// the production Dolt server (port 3307).
728+
// Returns a cleanup function that stops the server and removes the temp dir.
729+
func startTestDoltServer() func() {
730+
if _, err := exec.LookPath("dolt"); err != nil {
731+
fmt.Fprintln(os.Stderr, "WARN: dolt not found in PATH; regression tests will be skipped")
732+
return func() {}
733+
}
734+
735+
tmpDir, err := os.MkdirTemp("", "bd-regression-dolt-*")
736+
if err != nil {
737+
fmt.Fprintf(os.Stderr, "WARN: failed to create test dolt dir: %v\n", err)
738+
return func() {}
739+
}
740+
741+
dbDir := filepath.Join(tmpDir, "data")
742+
if err := os.MkdirAll(dbDir, 0755); err != nil {
743+
fmt.Fprintf(os.Stderr, "WARN: failed to create test dolt data dir: %v\n", err)
744+
_ = os.RemoveAll(tmpDir)
745+
return func() {}
746+
}
747+
748+
// Configure dolt user identity (required by dolt init).
749+
doltEnv := append(os.Environ(), "DOLT_ROOT_PATH="+tmpDir)
750+
for _, args := range [][]string{
751+
{"dolt", "config", "--global", "--add", "user.name", "regression-test"},
752+
{"dolt", "config", "--global", "--add", "user.email", "test@regression.test"},
753+
} {
754+
cfgCmd := exec.Command(args[0], args[1:]...)
755+
cfgCmd.Env = doltEnv
756+
if out, err := cfgCmd.CombinedOutput(); err != nil {
757+
fmt.Fprintf(os.Stderr, "WARN: %s failed: %v\n%s\n", args[1], err, out)
758+
_ = os.RemoveAll(tmpDir)
759+
return func() {}
760+
}
761+
}
762+
763+
initCmd := exec.Command("dolt", "init")
764+
initCmd.Dir = dbDir
765+
initCmd.Env = doltEnv
766+
if out, err := initCmd.CombinedOutput(); err != nil {
767+
fmt.Fprintf(os.Stderr, "WARN: dolt init failed for test server: %v\n%s\n", err, out)
768+
_ = os.RemoveAll(tmpDir)
769+
return func() {}
770+
}
771+
772+
port, err := findFreePort()
773+
if err != nil {
774+
fmt.Fprintf(os.Stderr, "WARN: failed to find free port for test dolt server: %v\n", err)
775+
_ = os.RemoveAll(tmpDir)
776+
return func() {}
777+
}
778+
779+
serverCmd := exec.Command("dolt", "sql-server",
780+
"-H", "127.0.0.1",
781+
"-P", fmt.Sprintf("%d", port),
782+
"--no-auto-commit",
783+
)
784+
serverCmd.Dir = dbDir
785+
serverCmd.Env = doltEnv
786+
if os.Getenv("BEADS_TEST_DOLT_VERBOSE") != "1" {
787+
serverCmd.Stderr = nil
788+
serverCmd.Stdout = nil
789+
}
790+
if err := serverCmd.Start(); err != nil {
791+
fmt.Fprintf(os.Stderr, "WARN: failed to start test dolt server: %v\n", err)
792+
_ = os.RemoveAll(tmpDir)
793+
return func() {}
794+
}
795+
796+
if !waitForServer(port, 10*time.Second) {
797+
fmt.Fprintf(os.Stderr, "WARN: test dolt server did not become ready on port %d\n", port)
798+
_ = serverCmd.Process.Kill()
799+
_ = serverCmd.Wait()
800+
_ = os.RemoveAll(tmpDir)
801+
return func() {}
802+
}
803+
804+
testDoltServerPort = port
805+
fmt.Fprintf(os.Stderr, "Test Dolt server running on port %d\n", port)
806+
807+
return func() {
808+
testDoltServerPort = 0
809+
_ = serverCmd.Process.Kill()
810+
_ = serverCmd.Wait()
811+
_ = os.RemoveAll(tmpDir)
812+
}
813+
}
814+
815+
// findFreePort finds an available TCP port by binding to :0.
816+
func findFreePort() (int, error) {
817+
l, err := net.Listen("tcp", "127.0.0.1:0")
818+
if err != nil {
819+
return 0, err
820+
}
821+
port := l.Addr().(*net.TCPAddr).Port
822+
_ = l.Close()
823+
return port, nil
824+
}
825+
826+
// waitForServer polls until the Dolt server accepts a MySQL connection.
827+
func waitForServer(port int, timeout time.Duration) bool {
828+
deadline := time.Now().Add(timeout)
829+
dsn := fmt.Sprintf("root@tcp(127.0.0.1:%d)/?timeout=1s", port)
830+
for time.Now().Before(deadline) {
831+
db, err := sql.Open("mysql", dsn)
832+
if err == nil {
833+
if err := db.Ping(); err == nil {
834+
_ = db.Close()
835+
return true
836+
}
837+
_ = db.Close()
838+
}
839+
time.Sleep(200 * time.Millisecond)
840+
}
841+
return false
842+
}

0 commit comments

Comments
 (0)