banger/internal/smoketest/scenarios_repodir_test.go
2026-05-01 19:34:44 -03:00

205 lines
8.1 KiB
Go

//go:build smoke
package smoketest
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
// testWorkspaceRun ports scenario_workspace_run. Ships the throwaway
// git repo to a fresh VM and reads the marker file from the guest.
func testWorkspaceRun(t *testing.T) {
out := mustBanger(t, "vm", "run", "--rm", repoDir, "--", "cat", "/root/repo/smoke-file.txt")
wantContains(t, out, "smoke-workspace-marker", "workspace vm run guest read")
}
// testWorkspaceDryrun ports scenario_workspace_dryrun. `--dry-run`
// lists the tracked files and the resolved transfer mode without
// creating a VM.
func testWorkspaceDryrun(t *testing.T) {
out := mustBanger(t, "vm", "run", "--dry-run", repoDir)
wantContains(t, out, "smoke-file.txt", "dry-run file list")
wantContains(t, out, "mode: tracked only", "dry-run mode line")
}
// testIncludeUntracked ports scenario_include_untracked. Drops an
// untracked file in the fixture and asserts --include-untracked picks
// it up. The cleanup hook removes the file even if the scenario fails
// so downstream repodir scenarios see the original tree.
func testIncludeUntracked(t *testing.T) {
untracked := filepath.Join(repoDir, "smoke-untracked.txt")
if err := os.WriteFile(untracked, []byte("untracked-marker\n"), 0o644); err != nil {
t.Fatalf("write untracked file: %v", err)
}
t.Cleanup(func() { _ = os.Remove(untracked) })
out := mustBanger(t, "vm", "run", "--rm", "--include-untracked", repoDir,
"--", "cat", "/root/repo/smoke-untracked.txt")
wantContains(t, out, "untracked-marker", "include-untracked guest read")
}
// testWorkspaceExport ports scenario_workspace_export. Round-trips a
// guest-side edit back out as a patch via `vm workspace export`.
func testWorkspaceExport(t *testing.T) {
const name = "smoke-export"
vmCreate(t, name, "--image", "debian-bookworm")
mustBanger(t, "vm", "workspace", "prepare", name, repoDir)
mustBanger(t, "vm", "ssh", name, "--", "sh", "-c",
"echo guest-edit > /root/repo/new-guest-file.txt")
patch := filepath.Join(runtimeDir, "smoke-export.diff")
mustBanger(t, "vm", "workspace", "export", name, "--output", patch)
st, err := os.Stat(patch)
if err != nil {
t.Fatalf("export: stat patch %s: %v", patch, err)
}
if st.Size() == 0 {
t.Fatalf("export: patch file empty at %s", patch)
}
body, err := os.ReadFile(patch)
if err != nil {
t.Fatalf("export: read patch: %v", err)
}
wantContains(t, string(body), "new-guest-file.txt", "export: patch must reference new-guest-file.txt")
}
// testWorkspaceFullCopy ports scenario_workspace_full_copy. Verifies
// the alternate transfer path (--mode full_copy) lands the same fixture
// in the guest.
func testWorkspaceFullCopy(t *testing.T) {
const name = "smoke-fc"
vmCreate(t, name)
mustBanger(t, "vm", "workspace", "prepare", name, repoDir, "--mode", "full_copy")
out := mustBanger(t, "vm", "ssh", name, "--", "cat", "/root/repo/smoke-file.txt")
wantContains(t, out, "smoke-workspace-marker", "full_copy: marker missing in guest")
}
// testWorkspaceBasecommit ports scenario_workspace_basecommit. Confirms
// that `vm workspace export` without --base-commit captures only the
// working-copy diff, while --base-commit also captures guest-side
// commits made on top of HEAD.
func testWorkspaceBasecommit(t *testing.T) {
const name = "smoke-basecommit"
vmCreate(t, name)
mustBanger(t, "vm", "workspace", "prepare", name, repoDir)
baseSHA := strings.TrimSpace(mustBanger(t, "vm", "ssh", name, "--",
"sh", "-c", "cd /root/repo && git rev-parse HEAD"))
if len(baseSHA) != 40 {
t.Fatalf("export base: bad base sha: %q", baseSHA)
}
mustBanger(t, "vm", "ssh", name, "--", "sh", "-c",
"cd /root/repo && "+
"git -c user.email=smoke@smoke -c user.name=smoke checkout -b smoke-branch >/dev/null 2>&1 && "+
"echo committed-marker > smoke-committed.txt && "+
"git add smoke-committed.txt && "+
"git -c user.email=smoke@smoke -c user.name=smoke commit -q -m 'guest side'")
plain := filepath.Join(runtimeDir, "smoke-plain.diff")
mustBanger(t, "vm", "workspace", "export", name, "--output", plain)
if body, err := os.ReadFile(plain); err == nil {
wantNotContains(t, string(body), "smoke-committed.txt",
"export base: plain export must NOT capture guest-side commit")
}
base := filepath.Join(runtimeDir, "smoke-base.diff")
mustBanger(t, "vm", "workspace", "export", name, "--base-commit", baseSHA, "--output", base)
st, err := os.Stat(base)
if err != nil || st.Size() == 0 {
t.Fatalf("export base: --base-commit patch empty/missing: stat=%v err=%v", st, err)
}
body, _ := os.ReadFile(base)
wantContains(t, string(body), "smoke-committed.txt",
"export base: --base-commit patch must include committed marker")
}
// testWorkspaceRestart ports scenario_workspace_restart. Verifies the
// workspace marker survives a stop/start cycle (rootfs persistence).
func testWorkspaceRestart(t *testing.T) {
const name = "smoke-wsrestart"
vmCreate(t, name)
mustBanger(t, "vm", "workspace", "prepare", name, repoDir)
pre := mustBanger(t, "vm", "ssh", name, "--", "cat", "/root/repo/smoke-file.txt")
wantContains(t, pre, "smoke-workspace-marker", "workspace stop/start: pre-cycle marker")
mustBanger(t, "vm", "stop", name)
mustBanger(t, "vm", "start", name)
waitForSSH(t, name)
post := mustBanger(t, "vm", "ssh", name, "--", "cat", "/root/repo/smoke-file.txt")
wantContains(t, post, "smoke-workspace-marker", "workspace stop/start: post-cycle marker")
}
// testVMExec ports scenario_vm_exec. The longest scenario in the suite
// — covers auto-cd, exit-code propagation, stale-workspace detection,
// --auto-prepare resync, and the not-running refusal. The repodir
// commit added mid-scenario is rolled back via t.Cleanup so subsequent
// repodir-chain scenarios see the original fixture state.
func testVMExec(t *testing.T) {
const name = "smoke-exec"
vmCreate(t, name)
mustBanger(t, "vm", "workspace", "prepare", name, repoDir)
show := mustBanger(t, "vm", "show", name)
wantContains(t, show, `"guest_path": "/root/repo"`,
"vm exec: workspace.guest_path not persisted")
out := mustBanger(t, "vm", "exec", name, "--", "cat", "smoke-file.txt")
wantContains(t, out, "smoke-workspace-marker", "vm exec: workspace marker")
if got := strings.TrimSpace(mustBanger(t, "vm", "exec", name, "--", "pwd")); got != "/root/repo" {
t.Fatalf("vm exec: pwd got %q, want /root/repo (auto-cd didn't happen)", got)
}
res := banger(t, "vm", "exec", name, "--", "sh", "-c", "exit 17")
wantExit(t, res, 17, "vm exec: exit-code propagation")
// Advance host HEAD so the workspace goes stale, register the
// rollback before mutating so a Fatal anywhere below still
// restores the fixture.
t.Cleanup(func() {
cmd := exec.Command("git", "reset", "--hard", "HEAD~1", "-q")
cmd.Dir = repoDir
_ = cmd.Run()
})
for _, args := range [][]string{
{"sh", "-c", "echo post-prepare-marker > smoke-exec-new.txt"},
{"git", "add", "smoke-exec-new.txt"},
{"git", "commit", "-q", "-m", "add smoke-exec-new.txt after prepare"},
} {
cmd := exec.Command(args[0], args[1:]...)
cmd.Dir = repoDir
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("vm exec: stage host commit: %s: %v\n%s", args, err, out)
}
}
stale := banger(t, "vm", "exec", name, "--", "ls", "smoke-exec-new.txt")
if stale.rc == 0 {
t.Fatalf("vm exec: stale workspace already had the new file (dirty path didn't take effect)")
}
wantContains(t, stale.stderr, "workspace stale", "vm exec: stale-workspace warning on stderr")
wantContains(t, stale.stderr, "--auto-prepare", "vm exec: stale warning must mention --auto-prepare")
auto := mustBanger(t, "vm", "exec", name, "--auto-prepare", "--", "cat", "smoke-exec-new.txt")
wantContains(t, auto, "post-prepare-marker", "vm exec: --auto-prepare didn't re-sync new file")
clean := banger(t, "vm", "exec", name, "--", "true")
wantExit(t, clean, 0, "vm exec: post-auto-prepare run")
wantNotContains(t, clean.stderr, "workspace stale", "vm exec: stale warning persisted after --auto-prepare")
mustBanger(t, "vm", "stop", name)
stopped := banger(t, "vm", "exec", name, "--", "true")
if stopped.rc == 0 {
t.Fatalf("vm exec: exec on stopped VM unexpectedly succeeded")
}
wantContains(t, stopped.stderr, "not running", "vm exec: stopped-VM error message")
}