mirror of
https://github.com/gastownhall/beads.git
synced 2026-09-14 20:17:24 +08:00
30f30ca95b
* test(ci): red — gastownhall/beads: no PR-CI lane runs TestBenchDBPurgeDoesNotLeak against a live Dolt server (checkDolt SKIP) (refs be-aiy5) * feat(ci): green — gastownhall/beads: no PR-CI lane runs TestBenchDBPurgeDoesNotLeak against a live Dolt server (checkDolt SKIP) (refs be-aiy5) Add test-server-storage-full as a sibling job to test-server-storage in pr-risk.yml. build-embedded already compiles every top-level test in internal/storage/dolt into one binary (embedded-test-binaries); the existing job only ever runs -test.run '^TestConformance$' against it. The new job reuses that same artifact and live-server setup with no new build step, and runs everything else via -test.skip '^TestConformance$' so previously-stranded tests (TestCreateGuard_*, federation/concurrent suites, and diff-owned tests such as TestBenchDBPurgeDoesNotLeak on PR #5792) get a real PASS/FAIL instead of a silent SKIP. Sets BEADS_TEST_ENV_RUN_DOLT=1 so federation_test.go's own live-server check fails loudly rather than skipping if this job's setup ever regresses. Wired into ci-gate: added to needs, CI_GATE_REQUIRED, and the tier-gated skipped_ok fallback (mirrors test-server-storage's existing treatment in each). Also updates two pre-existing meta-tests over pr-risk.yml that pinned exact snapshots of the file this job necessarily changes: scripts/ci_capability_selector_test.go's ci-gate authority snapshot, and scripts/pull_dolt_image_test.go's retry-helper call count (pr-risk.yml: 2 -> 3, the new job's own Dolt-image-pull step). * chore: release gate PASS for be-pp7e (PR-CI lane for TestBenchDBPurgeDoesNotLeak) Deploy gate record for be-pp7e, gating be-aiy5's PR-CI lane addition (runs TestBenchDBPurgeDoesNotLeak against a live Dolt server instead of letting checkDolt SKIP it). Review: be-w90n, verdict pass. All 7 gate criteria PASS; two pre-existing/unrelated failures encountered during evaluation (be-jy56, be-vf95) were attributed and excluded from the verdict. * test(ci): red — shard test-server-storage-full across 16 matrix jobs (refs be-yy25) TestPRRiskGateReachesFullServerDoltStorageSuite now asserts pr-risk.yml's test-server-storage-full job is a fail-fast:false matrix over shard 1..16, with the Test step invoking server-storage-test-shard.sh, instead of pinning the old single-job unsharded command. Fails against the current workflow, which still runs all 1126 tests unsharded in one 15m job (be-extn). * feat(ci): green — shard test-server-storage-full across 16 matrix jobs (refs be-yy25) Adds .github/scripts/server-storage-test-shard.sh (mirrors embedded-storage-test-shard.sh: discovers internal/storage/dolt/*_test.go, excludes TestConformance, manifest-plus-hash-fallback shard assignment) and its shard manifest .github/scripts/server-storage-test-shards.txt, pinning TestCloudAuthCLIRouting (~9.5m) alone on shard 1 and spreading the other known 10-20s-heavy tests across the rest. pr-risk.yml's test-server-storage-full job becomes a fail-fast:false matrix over shard 1..16, Test step now runs server-storage-test-shard.sh instead of the old unsharded single-job command that died at 256/1126 tests after 15m28s with zero failures (job 95616282508). Documents the new lane in engdocs/CI_REQUIRED_CHECK_TOPOLOGY.md alongside the existing embedded matrix description. TestPRRiskGateReachesFullServerDoltStorageSuite now passes. Offline shard partition verified disjoint and complete across all 1125 discovered tests (BEADS_TEST_SHARD_LIST_ONLY=1 for i=1..16). (be-extn) * test(feat): red — Fix: sharded lane runs prebuilt test binary from repo root, breaking ../schema/migrations and ../issueops (6 tests) (refs be-gepv) * feat: green — Fix: sharded lane runs prebuilt test binary from repo root, breaking ../schema/migrations and ../issueops (6 tests) (refs be-gepv) * chore: release gate PASS for be-23f3 Extends deploy/be-pp7e-gate (PR gastownhall/beads#5836) with be-gepv's fix rather than cutting a fresh isolated branch: be-gepv's own bead text and be-extn's precedent both establish this fix belongs on the already-open PR. All 7 gate criteria PASS; two repo-wide lint/policy failures attributed to already-tracked pre-existing issues (be-vf95, be-jy56), confirmed untouched by this PR lineage. be-gepv, be-6yo1, be-23f3 * fix(dolt): federation test isolation + panic (be-3c78s) TestFederationDatabaseIsolation and TestFederationVersionControlAPIs are federation-prototype tests that predate the suite's shared-server, branch-per-test isolation architecture (setupTestStore/StartTestBranch) and were never updated to account for it. - setupFederationStore hardcoded Database: "beads" for both towns. New() always connects in server mode against one shared Dolt server for the whole test binary (TestMain), so Path alone doesn't isolate two stores — Database does. Both towns silently shared one database, so Alpha could read Beta's issues and vice versa. Give each town its own database (beads_test_federation_<prefix>, matching the beads_test AD-01 cleanup-firewall prefix). - TestFederationDatabaseIsolation's not-found assertions assumed GetIssue returns (nil, nil) on a miss. It never does — issueops.GetIssueInTx always returns a wrapped storage.ErrNotFound. Switched to errors.Is(err, storage.ErrNotFound). - TestFederationVersionControlAPIs hardcoded Checkout(ctx, "main") to return to its "starting" branch, but setupTestStore already checks out a private branch (test-<name>-<hex>) — "main" is a branch shared by every parallel test in the binary and never has vc-001. That mismatch is what produced the nil-pointer panic (GetIssue's error was discarded via `_`, and the resulting nil issue was dereferenced). Capture the real starting branch via CurrentBranch() right after setupTestStore() and check both previously-discarded GetIssue errors before dereferencing. Neither bug is a production security issue: New(), Checkout(), and GetIssue() all behave exactly as specified. Both bugs are confined to this prototype test file's own setup/assertions, which hadn't caught up to the isolation model the rest of the suite already uses correctly. Verified via `go build`/`go vet`/`go test -c` (all clean) and running the compiled binary, which now reaches and cleanly SKIPs both tests (no Docker locally) instead of the previous isolation failure / panic surfacing at compile-adjacent runtime. Full GREEN confirmation is via CI, which has a working Dolt/Docker environment. Refs be-3c78s * chore: release gate PASS for be-42tlj
204 lines
5.4 KiB
Go
204 lines
5.4 KiB
Go
package scripts_test
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const doltSQLServerImage = "dolthub/dolt-sql-server:2.2.0"
|
|
|
|
func TestPullDoltImageRetriesTransientFailures(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
failures int
|
|
wantExit int
|
|
wantPulls int
|
|
wantSleeps int
|
|
}{
|
|
{name: "immediate success", failures: 0, wantExit: 0, wantPulls: 1, wantSleeps: 0},
|
|
{name: "eventual success", failures: 2, wantExit: 0, wantPulls: 3, wantSleeps: 2},
|
|
{name: "exhausted retries", failures: 3, wantExit: 42, wantPulls: 3, wantSleeps: 2},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
run := runPullDoltImage(t, test.failures)
|
|
if got := pullDoltExitCode(run.err); got != test.wantExit {
|
|
t.Fatalf("exit = %d, want %d; error=%v\n%s", got, test.wantExit, run.err, run.output)
|
|
}
|
|
if len(run.dockerCalls) != test.wantPulls {
|
|
t.Fatalf("docker pull count = %d, want %d; calls=%q\n%s", len(run.dockerCalls), test.wantPulls, run.dockerCalls, run.output)
|
|
}
|
|
for _, call := range run.dockerCalls {
|
|
if call != "pull "+doltSQLServerImage {
|
|
t.Fatalf("docker call = %q, want %q", call, "pull "+doltSQLServerImage)
|
|
}
|
|
}
|
|
if len(run.sleepCalls) != test.wantSleeps {
|
|
t.Fatalf("sleep count = %d, want %d; calls=%q\n%s", len(run.sleepCalls), test.wantSleeps, run.sleepCalls, run.output)
|
|
}
|
|
for _, call := range run.sleepCalls {
|
|
if call != "5" {
|
|
t.Fatalf("sleep call = %q, want 5", call)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDoltImagePullWorkflowsUseRetryHelper(t *testing.T) {
|
|
wantCalls := map[string]int{
|
|
"main.yml": 2,
|
|
"pr.yml": 2,
|
|
"pr-risk.yml": 3,
|
|
"regression.yml": 1,
|
|
}
|
|
|
|
workflowsDir := filepath.Join(sourceRepoRoot(t), ".github", "workflows")
|
|
for name, want := range wantCalls {
|
|
data, err := os.ReadFile(filepath.Join(workflowsDir, name))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
text := string(data)
|
|
if got := strings.Count(text, "run: ./scripts/ci/pull-dolt-image.sh"); got != want {
|
|
t.Errorf("%s retry-helper calls = %d, want %d", name, got, want)
|
|
}
|
|
}
|
|
|
|
workflowPaths, err := filepath.Glob(filepath.Join(workflowsDir, "*.y*ml"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, path := range workflowPaths {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(data), "docker pull "+doltSQLServerImage) {
|
|
t.Errorf("%s still pulls the Dolt image without retries", filepath.Base(path))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPullDoltImageHelperIsExecutable(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("Windows does not preserve Unix executable bits")
|
|
}
|
|
|
|
path := filepath.Join(sourceRepoRoot(t), "scripts", "ci", "pull-dolt-image.sh")
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if info.Mode().Perm()&0o111 == 0 {
|
|
t.Fatalf("%s is not executable", path)
|
|
}
|
|
}
|
|
|
|
type pullDoltRun struct {
|
|
output string
|
|
dockerCalls []string
|
|
sleepCalls []string
|
|
err error
|
|
}
|
|
|
|
func runPullDoltImage(t *testing.T, failures int) pullDoltRun {
|
|
t.Helper()
|
|
|
|
bash, err := exec.LookPath("bash")
|
|
if err != nil {
|
|
t.Skipf("bash is required to test pull-dolt-image.sh: %v", err)
|
|
}
|
|
|
|
bin := t.TempDir()
|
|
stateDir := t.TempDir()
|
|
dockerLog := filepath.Join(stateDir, "docker-calls")
|
|
sleepLog := filepath.Join(stateDir, "sleep-calls")
|
|
for _, path := range []string{dockerLog, sleepLog} {
|
|
if err := os.WriteFile(path, nil, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
writeExecutable(t, filepath.Join(bin, "docker"), `#!/bin/sh
|
|
set -eu
|
|
count=0
|
|
if [ -s "$DOLT_PULL_CALL_COUNT" ]; then
|
|
IFS= read -r count <"$DOLT_PULL_CALL_COUNT"
|
|
fi
|
|
count=$((count + 1))
|
|
printf '%s\n' "$count" >"$DOLT_PULL_CALL_COUNT"
|
|
printf '%s\n' "$*" >>"$DOLT_PULL_DOCKER_LOG"
|
|
if [ "$count" -le "$DOLT_PULL_FAILURES" ]; then
|
|
exit 42
|
|
fi
|
|
`)
|
|
writeExecutable(t, filepath.Join(bin, "sleep"), `#!/bin/sh
|
|
set -eu
|
|
printf '%s\n' "$*" >>"$DOLT_PULL_SLEEP_LOG"
|
|
`)
|
|
|
|
pathEnv := shellPathEnv()
|
|
binPath := shellPathUnderEnv(t, bash, bin, pathEnv)
|
|
statePath := shellPathUnderEnv(t, bash, stateDir, pathEnv)
|
|
commandPath := binPath + ":" + os.Getenv("PATH") + ":/usr/bin:/bin"
|
|
if runtime.GOOS == "windows" {
|
|
commandPath = binPath + ":/usr/bin:/bin"
|
|
}
|
|
root := sourceRepoRoot(t)
|
|
env := []string{
|
|
"PATH=" + os.Getenv("PATH"),
|
|
"BEADS_TEST_COMMAND_PATH=" + commandPath,
|
|
"LC_ALL=C",
|
|
"LANG=C",
|
|
"BASH_ENV=",
|
|
"ENV=",
|
|
"DOLT_PULL_CALL_COUNT=" + statePath + "/docker-call-count",
|
|
"DOLT_PULL_DOCKER_LOG=" + statePath + "/docker-calls",
|
|
"DOLT_PULL_SLEEP_LOG=" + statePath + "/sleep-calls",
|
|
"DOLT_PULL_FAILURES=" + strconv.Itoa(failures),
|
|
}
|
|
requireShellCommandPath(t, bash, root, env, "docker", binPath+"/docker")
|
|
requireShellCommandPath(t, bash, root, env, "sleep", binPath+"/sleep")
|
|
|
|
cmd := bashScriptCommand(bash, "scripts/ci/pull-dolt-image.sh")
|
|
cmd.Dir = root
|
|
cmd.Env = env
|
|
output, runErr := cmd.CombinedOutput()
|
|
|
|
dockerCalls := readCallLines(t, dockerLog)
|
|
sleepCalls := readCallLines(t, sleepLog)
|
|
return pullDoltRun{output: string(output), dockerCalls: dockerCalls, sleepCalls: sleepCalls, err: runErr}
|
|
}
|
|
|
|
func readCallLines(t *testing.T, path string) []string {
|
|
t.Helper()
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
text := strings.TrimSpace(string(data))
|
|
if text == "" {
|
|
return nil
|
|
}
|
|
return strings.Split(text, "\n")
|
|
}
|
|
|
|
func pullDoltExitCode(err error) int {
|
|
if err == nil {
|
|
return 0
|
|
}
|
|
var exitErr *exec.ExitError
|
|
if errors.As(err, &exitErr) {
|
|
return exitErr.ExitCode()
|
|
}
|
|
return -1
|
|
}
|