port smoke to go
This commit is contained in:
parent
b0a9d64f4a
commit
9ed44bfd75
20 changed files with 2118 additions and 1573 deletions
201
internal/smoketest/helpers_test.go
Normal file
201
internal/smoketest/helpers_test.go
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
//go:build smoke
|
||||
|
||||
package smoketest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// result captures the output and exit status of a banger invocation.
|
||||
// stdout / stderr are kept separate so assertions can target one or the
|
||||
// other (matches the bash suite's `out=$(cmd)` vs `2>&1` patterns).
|
||||
type result struct {
|
||||
stdout string
|
||||
stderr string
|
||||
rc int
|
||||
}
|
||||
|
||||
// runCmd executes the given exec.Cmd, capturing stdout and stderr into
|
||||
// the returned result. Non-zero exits are returned as a non-zero rc, not
|
||||
// as an error — scenarios decide for themselves whether non-zero is a
|
||||
// failure or the assertion under test.
|
||||
func runCmd(t *testing.T, cmd *exec.Cmd) result {
|
||||
t.Helper()
|
||||
var outBuf, errBuf bytes.Buffer
|
||||
cmd.Stdout = &outBuf
|
||||
cmd.Stderr = &errBuf
|
||||
err := cmd.Run()
|
||||
res := result{stdout: outBuf.String(), stderr: errBuf.String()}
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
res.rc = exitErr.ExitCode()
|
||||
} else {
|
||||
t.Fatalf("exec %s: %v\nstderr: %s", strings.Join(cmd.Args, " "), err, res.stderr)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// banger runs the instrumented `banger` binary with the given arguments
|
||||
// and returns the captured result. GOCOVERDIR is inherited from the
|
||||
// process environment (TestMain exports it), so child covdata lands
|
||||
// under BANGER_SMOKE_COVER_DIR automatically.
|
||||
func banger(t *testing.T, args ...string) result {
|
||||
t.Helper()
|
||||
return runCmd(t, exec.Command(bangerBin, args...))
|
||||
}
|
||||
|
||||
// mustBanger runs `banger` and Fatals if it exits non-zero. Returns the
|
||||
// captured stdout for downstream `wantContains`. Most happy-path
|
||||
// scenarios use this; scenarios that assert on non-zero exits use
|
||||
// banger() directly.
|
||||
func mustBanger(t *testing.T, args ...string) string {
|
||||
t.Helper()
|
||||
res := banger(t, args...)
|
||||
if res.rc != 0 {
|
||||
t.Fatalf("banger %s: exit %d\nstdout: %s\nstderr: %s",
|
||||
strings.Join(args, " "), res.rc, res.stdout, res.stderr)
|
||||
}
|
||||
return res.stdout
|
||||
}
|
||||
|
||||
// sudoBanger runs `banger` under `sudo env GOCOVERDIR=...`. Sudo strips
|
||||
// the env by default; explicit re-export keeps coverage flowing for
|
||||
// scenarios that exercise the privileged path (system install / restart
|
||||
// / update / daemon stop).
|
||||
func sudoBanger(t *testing.T, args ...string) result {
|
||||
t.Helper()
|
||||
full := append([]string{"env", "GOCOVERDIR=" + coverDir, bangerBin}, args...)
|
||||
return runCmd(t, exec.Command("sudo", full...))
|
||||
}
|
||||
|
||||
// wantContains asserts that haystack contains needle. label is a short
|
||||
// human-readable identifier for the failure message.
|
||||
func wantContains(t *testing.T, haystack, needle, label string) {
|
||||
t.Helper()
|
||||
if !strings.Contains(haystack, needle) {
|
||||
t.Fatalf("%s missing %q\ngot: %s", label, needle, haystack)
|
||||
}
|
||||
}
|
||||
|
||||
// wantNotContains is the negative-assertion counterpart. Used by
|
||||
// scenarios that verify a warning has been suppressed (e.g. the post-
|
||||
// auto-prepare clean-state check in vm_exec) or that an export patch
|
||||
// did NOT capture a guest-side commit.
|
||||
func wantNotContains(t *testing.T, haystack, needle, label string) {
|
||||
t.Helper()
|
||||
if strings.Contains(haystack, needle) {
|
||||
t.Fatalf("%s unexpectedly contains %q\ngot: %s", label, needle, haystack)
|
||||
}
|
||||
}
|
||||
|
||||
// wantExit asserts the captured result exited with want. Used for
|
||||
// scenarios that test exit-code propagation or refusal paths.
|
||||
func wantExit(t *testing.T, got result, want int, label string) {
|
||||
t.Helper()
|
||||
if got.rc != want {
|
||||
t.Fatalf("%s: exit %d, want %d\nstdout: %s\nstderr: %s",
|
||||
label, got.rc, want, got.stdout, got.stderr)
|
||||
}
|
||||
}
|
||||
|
||||
// vmDelete removes a VM, ignoring failure. Used in t.Cleanup hooks
|
||||
// where the VM may already be gone (deleted by the scenario itself).
|
||||
func vmDelete(name string) {
|
||||
cmd := exec.Command(bangerBin, "vm", "delete", name)
|
||||
_ = cmd.Run()
|
||||
}
|
||||
|
||||
// vmCreate creates a VM with the given name and registers a cleanup
|
||||
// hook to delete it. extraArgs is forwarded after `vm create --name X`
|
||||
// so callers can pass --vcpu N / --nat / --no-start / etc. Fatals if
|
||||
// creation fails — every scenario that uses vmCreate needs the VM up.
|
||||
func vmCreate(t *testing.T, name string, extraArgs ...string) {
|
||||
t.Helper()
|
||||
args := append([]string{"vm", "create", "--name", name}, extraArgs...)
|
||||
mustBanger(t, args...)
|
||||
t.Cleanup(func() { vmDelete(name) })
|
||||
}
|
||||
|
||||
// bangerHome runs `banger` with HOME overridden to the given directory.
|
||||
// Used by ssh-config scenarios that mutate ~/.ssh/config under a fake
|
||||
// home so the test doesn't touch the contributor's real config.
|
||||
func bangerHome(t *testing.T, home string, args ...string) result {
|
||||
t.Helper()
|
||||
cmd := exec.Command(bangerBin, args...)
|
||||
cmd.Env = append(os.Environ(), "HOME="+home)
|
||||
return runCmd(t, cmd)
|
||||
}
|
||||
|
||||
// mustBangerHome is bangerHome + Fatal-on-non-zero. Returns stdout.
|
||||
func mustBangerHome(t *testing.T, home string, args ...string) string {
|
||||
t.Helper()
|
||||
res := bangerHome(t, home, args...)
|
||||
if res.rc != 0 {
|
||||
t.Fatalf("banger %s (HOME=%s): exit %d\nstdout: %s\nstderr: %s",
|
||||
strings.Join(args, " "), home, res.rc, res.stdout, res.stderr)
|
||||
}
|
||||
return res.stdout
|
||||
}
|
||||
|
||||
// waitForSSH polls `banger vm ssh <name> -- true` until SSH answers,
|
||||
// up to 120 seconds. The original bash suite used 60s and occasionally
|
||||
// flaked under load (post-update VM, large parallel pool); 120s gives
|
||||
// enough headroom for the post-update / post-rollback paths where the
|
||||
// daemon has just restarted, without making genuine breakage slow to
|
||||
// surface.
|
||||
func waitForSSH(t *testing.T, name string) {
|
||||
t.Helper()
|
||||
const timeout = 120 * time.Second
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
cmd := exec.Command(bangerBin, "vm", "ssh", name, "--", "true")
|
||||
if err := cmd.Run(); err == nil {
|
||||
return
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
t.Fatalf("vm %q ssh did not come up within %s", name, timeout)
|
||||
}
|
||||
|
||||
// requirePasswordlessSudo skips the test if `sudo -n true` cannot run.
|
||||
// Mirrors the bash `if ! sudo -n true 2>/dev/null; then return 0; fi`
|
||||
// pattern used by scenarios that exercise privileged paths.
|
||||
func requirePasswordlessSudo(t *testing.T) {
|
||||
t.Helper()
|
||||
if err := exec.Command("sudo", "-n", "true").Run(); err != nil {
|
||||
t.Skip("passwordless sudo unavailable")
|
||||
}
|
||||
}
|
||||
|
||||
// requireSudoIptables skips the test if iptables can't be queried under
|
||||
// `sudo -n`. Used by the NAT scenario whose assertions read POSTROUTING.
|
||||
func requireSudoIptables(t *testing.T) {
|
||||
t.Helper()
|
||||
if err := exec.Command("sudo", "-n", "iptables", "-t", "nat", "-S", "POSTROUTING").Run(); err != nil {
|
||||
t.Skip("passwordless sudo iptables unavailable")
|
||||
}
|
||||
}
|
||||
|
||||
// installedVersion reads `/usr/local/bin/banger --version` and returns
|
||||
// the version token. This is the *installed* binary that `banger update`
|
||||
// swaps out — the smoke CLI under $BANGER_SMOKE_BIN_DIR is separate
|
||||
// (and unaffected by update). Mirrors the bash `installed_version`
|
||||
// helper at scripts/smoke.sh:1156-1162.
|
||||
func installedVersion(t *testing.T) string {
|
||||
t.Helper()
|
||||
out, err := exec.Command("/usr/local/bin/banger", "--version").Output()
|
||||
if err != nil {
|
||||
t.Fatalf("read installed version: %v", err)
|
||||
}
|
||||
parts := strings.Fields(string(out))
|
||||
if len(parts) < 2 {
|
||||
t.Fatalf("unparseable installed --version output: %q", string(out))
|
||||
}
|
||||
return parts[1]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue