seams: move the last four package globals onto instance fields
Three test seams were still package-level mutable vars, which tests
had to swap before use. That's the classic path to flaky parallel
tests — two goroutines fighting over the same global fake. Push each
down to the struct that owns the behaviour.
internal/daemon/dns_routing.go
lookupExecutableFunc + vmDNSAddrFunc → fields on *HostNetwork,
defaulted at newHostNetwork time. dns_routing_test builds
HostNetwork{..., lookupExecutable: stub, vmDNSAddr: stub} inline,
no more t.Cleanup dance around package-level vars.
internal/daemon/preflight.go + doctor.go
vsockHostDevicePath (mutable string) → vsockHostDevice field on
*VMService, defaulted via defaultVsockHostDevice constant in
newVMService. Preflight reads s.vsockHostDevice; doctor reads
d.vm.vsockHostDevice. Logger test sets d.vm.vsockHostDevice = tmp
after wireServices.
internal/daemon/workspace/workspace.go
HostCommandOutputFunc → *Inspector struct with a Runner field.
Every git-using helper (GitOutput, GitTrimmedOutput,
GitResolvedConfigValue, RunHostCommand, ListSubmodules,
ListOverlayPaths, CountUntrackedPaths, InspectRepo,
ImportRepoToGuest, PrepareRepoCopy) is now a method on *Inspector.
NewInspector() wraps the real host runner for production;
WorkspaceService holds one via repoInspector, CLI deps holds one
too. cli_test.go's submodule-rejection test builds its own
Inspector with a scripted Runner instead of patching a global.
Pure helpers (FinalizeScript, ResolveSourcePath, ParsePrepareMode,
ShellQuote, FormatStepError, GitFileURL, ParseNullSeparatedOutput)
stay free functions since they don't touch the host.
Sentinel: grep for HostCommandOutputFunc, lookupExecutableFunc,
vmDNSAddrFunc, vsockHostDevicePath is now empty across internal/.
make lint test green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2685bc73f8
commit
ecb18ce6ca
17 changed files with 201 additions and 137 deletions
|
|
@ -2,6 +2,12 @@
|
|||
// git repo inspection, shallow copy preparation, guest-side tar import,
|
||||
// finalization script generation, and small utilities.
|
||||
//
|
||||
// Every helper that needs to run a host command (git or otherwise)
|
||||
// lives as a method on *Inspector rather than a free function that
|
||||
// routes through a package global. That way two tests running in
|
||||
// parallel can each build their own Inspector with a stub Runner
|
||||
// without fighting over shared state.
|
||||
//
|
||||
// The orchestrator methods (ExportVMWorkspace, PrepareVMWorkspace) stay on
|
||||
// *daemon.Daemon.
|
||||
package workspace
|
||||
|
|
@ -51,9 +57,28 @@ type GuestClient interface {
|
|||
StreamTarEntries(ctx context.Context, dir string, entries []string, command string, log io.Writer) error
|
||||
}
|
||||
|
||||
// HostCommandOutputFunc runs a host command and returns its combined output.
|
||||
// Declared as a package var so tests can substitute a stub runner.
|
||||
var HostCommandOutputFunc = func(ctx context.Context, name string, args ...string) ([]byte, error) {
|
||||
// RunnerFunc is the single-method surface every Inspector needs: run a
|
||||
// host command with args, return combined output + error. Tests supply
|
||||
// a stub that records calls and replays canned responses; production
|
||||
// uses realHostRunner which wraps system.NewRunner.
|
||||
type RunnerFunc func(ctx context.Context, name string, args ...string) ([]byte, error)
|
||||
|
||||
// Inspector bundles the host-command seam for all git-using workspace
|
||||
// helpers. Construct one at the boundary where you're reading the
|
||||
// filesystem (CLI deps, WorkspaceService) and call its methods directly;
|
||||
// don't reach into the struct from helper code.
|
||||
type Inspector struct {
|
||||
Runner RunnerFunc
|
||||
}
|
||||
|
||||
// NewInspector returns an Inspector backed by the real host runner.
|
||||
// Production callers (CLI deps initialisation, daemon WorkspaceService
|
||||
// wiring) use this; tests construct Inspector{Runner: stub} directly.
|
||||
func NewInspector() *Inspector {
|
||||
return &Inspector{Runner: realHostRunner}
|
||||
}
|
||||
|
||||
func realHostRunner(ctx context.Context, name string, args ...string) ([]byte, error) {
|
||||
runner := system.NewRunner()
|
||||
output, err := runner.Run(ctx, name, args...)
|
||||
if err == nil {
|
||||
|
|
@ -72,55 +97,55 @@ var HostCommandOutputFunc = func(ctx context.Context, name string, args ...strin
|
|||
// submodules, and overlay paths needed for a prepare. Overlay paths
|
||||
// cover tracked files by default; untracked non-ignored files are
|
||||
// included only when includeUntracked is true.
|
||||
func InspectRepo(ctx context.Context, rawPath, branchName, fromRef string, includeUntracked bool) (RepoSpec, error) {
|
||||
func (i *Inspector) InspectRepo(ctx context.Context, rawPath, branchName, fromRef string, includeUntracked bool) (RepoSpec, error) {
|
||||
sourcePath, err := ResolveSourcePath(rawPath)
|
||||
if err != nil {
|
||||
return RepoSpec{}, err
|
||||
}
|
||||
repoRoot, err := GitTrimmedOutput(ctx, sourcePath, "rev-parse", "--show-toplevel")
|
||||
repoRoot, err := i.GitTrimmedOutput(ctx, sourcePath, "rev-parse", "--show-toplevel")
|
||||
if err != nil {
|
||||
return RepoSpec{}, fmt.Errorf("%s is not inside a git repository", sourcePath)
|
||||
}
|
||||
isBare, err := GitTrimmedOutput(ctx, repoRoot, "rev-parse", "--is-bare-repository")
|
||||
isBare, err := i.GitTrimmedOutput(ctx, repoRoot, "rev-parse", "--is-bare-repository")
|
||||
if err != nil {
|
||||
return RepoSpec{}, fmt.Errorf("inspect git repository %s: %w", repoRoot, err)
|
||||
}
|
||||
if isBare == "true" {
|
||||
return RepoSpec{}, fmt.Errorf("workspace prepare requires a non-bare git repository: %s", repoRoot)
|
||||
}
|
||||
submodules, err := ListSubmodules(ctx, repoRoot)
|
||||
submodules, err := i.ListSubmodules(ctx, repoRoot)
|
||||
if err != nil {
|
||||
return RepoSpec{}, err
|
||||
}
|
||||
headCommit, err := GitTrimmedOutput(ctx, repoRoot, "rev-parse", "HEAD^{commit}")
|
||||
headCommit, err := i.GitTrimmedOutput(ctx, repoRoot, "rev-parse", "HEAD^{commit}")
|
||||
if err != nil {
|
||||
return RepoSpec{}, fmt.Errorf("git repository %s must have at least one commit", repoRoot)
|
||||
}
|
||||
currentBranch, err := GitTrimmedOutput(ctx, repoRoot, "branch", "--show-current")
|
||||
currentBranch, err := i.GitTrimmedOutput(ctx, repoRoot, "branch", "--show-current")
|
||||
if err != nil {
|
||||
return RepoSpec{}, fmt.Errorf("resolve current branch for %s: %w", repoRoot, err)
|
||||
}
|
||||
baseCommit := headCommit
|
||||
branchName = strings.TrimSpace(branchName)
|
||||
if branchName != "" {
|
||||
baseCommit, err = GitTrimmedOutput(ctx, repoRoot, "rev-parse", fromRef+"^{commit}")
|
||||
baseCommit, err = i.GitTrimmedOutput(ctx, repoRoot, "rev-parse", fromRef+"^{commit}")
|
||||
if err != nil {
|
||||
return RepoSpec{}, fmt.Errorf("resolve workspace from %q: %w", fromRef, err)
|
||||
}
|
||||
}
|
||||
gitUserName, err := GitResolvedConfigValue(ctx, repoRoot, "user.name")
|
||||
gitUserName, err := i.GitResolvedConfigValue(ctx, repoRoot, "user.name")
|
||||
if err != nil {
|
||||
return RepoSpec{}, fmt.Errorf("resolve git user.name for %s: %w", repoRoot, err)
|
||||
}
|
||||
gitUserEmail, err := GitResolvedConfigValue(ctx, repoRoot, "user.email")
|
||||
gitUserEmail, err := i.GitResolvedConfigValue(ctx, repoRoot, "user.email")
|
||||
if err != nil {
|
||||
return RepoSpec{}, fmt.Errorf("resolve git user.email for %s: %w", repoRoot, err)
|
||||
}
|
||||
originURL, err := GitResolvedConfigValue(ctx, repoRoot, "remote.origin.url")
|
||||
originURL, err := i.GitResolvedConfigValue(ctx, repoRoot, "remote.origin.url")
|
||||
if err != nil {
|
||||
return RepoSpec{}, fmt.Errorf("resolve origin url for %s: %w", repoRoot, err)
|
||||
}
|
||||
overlayPaths, err := ListOverlayPaths(ctx, repoRoot, includeUntracked)
|
||||
overlayPaths, err := i.ListOverlayPaths(ctx, repoRoot, includeUntracked)
|
||||
if err != nil {
|
||||
return RepoSpec{}, err
|
||||
}
|
||||
|
|
@ -142,7 +167,7 @@ func InspectRepo(ctx context.Context, rawPath, branchName, fromRef string, inclu
|
|||
|
||||
// ImportRepoToGuest materialises spec inside the guest at guestPath. Mode
|
||||
// selects between full copy, metadata-only, or shallow metadata + overlay.
|
||||
func ImportRepoToGuest(ctx context.Context, client GuestClient, spec RepoSpec, guestPath string, mode model.WorkspacePrepareMode) error {
|
||||
func (i *Inspector) ImportRepoToGuest(ctx context.Context, client GuestClient, spec RepoSpec, guestPath string, mode model.WorkspacePrepareMode) error {
|
||||
switch mode {
|
||||
case model.WorkspacePrepareModeFullCopy:
|
||||
var copyLog bytes.Buffer
|
||||
|
|
@ -156,7 +181,7 @@ func ImportRepoToGuest(ctx context.Context, client GuestClient, spec RepoSpec, g
|
|||
}
|
||||
return nil
|
||||
case model.WorkspacePrepareModeMetadataOnly, model.WorkspacePrepareModeShallowOverlay:
|
||||
repoCopyDir, cleanup, err := PrepareRepoCopy(ctx, spec)
|
||||
repoCopyDir, cleanup, err := i.PrepareRepoCopy(ctx, spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -212,7 +237,7 @@ func FinalizeScript(spec RepoSpec, guestPath string, mode model.WorkspacePrepare
|
|||
|
||||
// PrepareRepoCopy materialises a shallow clone of spec into a temp dir. The
|
||||
// returned cleanup removes the temp root.
|
||||
func PrepareRepoCopy(ctx context.Context, spec RepoSpec) (string, func(), error) {
|
||||
func (i *Inspector) PrepareRepoCopy(ctx context.Context, spec RepoSpec) (string, func(), error) {
|
||||
tempRoot, err := os.MkdirTemp("", "banger-workspace-*")
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
|
|
@ -224,7 +249,7 @@ func PrepareRepoCopy(ctx context.Context, spec RepoSpec) (string, func(), error)
|
|||
cloneArgs = append(cloneArgs, "--single-branch", "--branch", spec.CurrentBranch)
|
||||
}
|
||||
cloneArgs = append(cloneArgs, GitFileURL(spec.RepoRoot), repoCopyDir)
|
||||
if err := RunHostCommand(ctx, "git", cloneArgs...); err != nil {
|
||||
if err := i.RunHostCommand(ctx, "git", cloneArgs...); err != nil {
|
||||
cleanup()
|
||||
return "", nil, fmt.Errorf("clone shallow workspace repo copy: %w", err)
|
||||
}
|
||||
|
|
@ -232,19 +257,19 @@ func PrepareRepoCopy(ctx context.Context, spec RepoSpec) (string, func(), error)
|
|||
if strings.TrimSpace(spec.BranchName) != "" {
|
||||
checkoutCommit = spec.BaseCommit
|
||||
}
|
||||
if err := RunHostCommand(ctx, "git", "-C", repoCopyDir, "cat-file", "-e", checkoutCommit+"^{commit}"); err != nil {
|
||||
if err := RunHostCommand(ctx, "git", "-C", repoCopyDir, "fetch", "--depth", fmt.Sprintf("%d", ShallowFetchDepth), GitFileURL(spec.RepoRoot), checkoutCommit); err != nil {
|
||||
if err := i.RunHostCommand(ctx, "git", "-C", repoCopyDir, "cat-file", "-e", checkoutCommit+"^{commit}"); err != nil {
|
||||
if err := i.RunHostCommand(ctx, "git", "-C", repoCopyDir, "fetch", "--depth", fmt.Sprintf("%d", ShallowFetchDepth), GitFileURL(spec.RepoRoot), checkoutCommit); err != nil {
|
||||
cleanup()
|
||||
return "", nil, fmt.Errorf("fetch shallow workspace repo commit %s: %w", checkoutCommit, err)
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(spec.OriginURL) != "" {
|
||||
if err := RunHostCommand(ctx, "git", "-C", repoCopyDir, "remote", "set-url", "origin", spec.OriginURL); err != nil {
|
||||
if err := i.RunHostCommand(ctx, "git", "-C", repoCopyDir, "remote", "set-url", "origin", spec.OriginURL); err != nil {
|
||||
cleanup()
|
||||
return "", nil, fmt.Errorf("set workspace origin remote: %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := RunHostCommand(ctx, "git", "-C", repoCopyDir, "remote", "remove", "origin"); err != nil {
|
||||
if err := i.RunHostCommand(ctx, "git", "-C", repoCopyDir, "remote", "remove", "origin"); err != nil {
|
||||
cleanup()
|
||||
return "", nil, fmt.Errorf("remove workspace placeholder origin remote: %w", err)
|
||||
}
|
||||
|
|
@ -273,8 +298,8 @@ func ResolveSourcePath(rawPath string) (string, error) {
|
|||
}
|
||||
|
||||
// ListSubmodules returns the gitlink paths in repoRoot (mode 160000 entries).
|
||||
func ListSubmodules(ctx context.Context, repoRoot string) ([]string, error) {
|
||||
output, err := GitOutput(ctx, repoRoot, "ls-files", "--stage", "-z")
|
||||
func (i *Inspector) ListSubmodules(ctx context.Context, repoRoot string) ([]string, error) {
|
||||
output, err := i.GitOutput(ctx, repoRoot, "ls-files", "--stage", "-z")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("inspect workspace git index for %s: %w", repoRoot, err)
|
||||
}
|
||||
|
|
@ -304,8 +329,8 @@ func ListSubmodules(ctx context.Context, repoRoot string) ([]string, error) {
|
|||
// leave the developer's machine. Callers that genuinely want the
|
||||
// fuller set (scratch repos, vendored binaries the user is iterating
|
||||
// on) opt in explicitly.
|
||||
func ListOverlayPaths(ctx context.Context, repoRoot string, includeUntracked bool) ([]string, error) {
|
||||
trackedOutput, err := GitOutput(ctx, repoRoot, "ls-files", "-z")
|
||||
func (i *Inspector) ListOverlayPaths(ctx context.Context, repoRoot string, includeUntracked bool) ([]string, error) {
|
||||
trackedOutput, err := i.GitOutput(ctx, repoRoot, "ls-files", "-z")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list tracked files for %s: %w", repoRoot, err)
|
||||
}
|
||||
|
|
@ -325,7 +350,7 @@ func ListOverlayPaths(ctx context.Context, repoRoot string, includeUntracked boo
|
|||
paths = append(paths, relPath)
|
||||
}
|
||||
if includeUntracked {
|
||||
untrackedOutput, err := GitOutput(ctx, repoRoot, "ls-files", "--others", "--exclude-standard", "-z")
|
||||
untrackedOutput, err := i.GitOutput(ctx, repoRoot, "ls-files", "--others", "--exclude-standard", "-z")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list untracked files for %s: %w", repoRoot, err)
|
||||
}
|
||||
|
|
@ -348,8 +373,8 @@ func ListOverlayPaths(ctx context.Context, repoRoot string, includeUntracked boo
|
|||
// files in repoRoot. Used by the CLI to warn the user when they are
|
||||
// about to ship a workspace that has local-but-unignored scratch
|
||||
// files which, under the default, will be skipped.
|
||||
func CountUntrackedPaths(ctx context.Context, repoRoot string) (int, error) {
|
||||
untrackedOutput, err := GitOutput(ctx, repoRoot, "ls-files", "--others", "--exclude-standard", "-z")
|
||||
func (i *Inspector) CountUntrackedPaths(ctx context.Context, repoRoot string) (int, error) {
|
||||
untrackedOutput, err := i.GitOutput(ctx, repoRoot, "ls-files", "--others", "--exclude-standard", "-z")
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("list untracked files for %s: %w", repoRoot, err)
|
||||
}
|
||||
|
|
@ -377,18 +402,18 @@ func ParsePrepareMode(raw string) (model.WorkspacePrepareMode, error) {
|
|||
}
|
||||
|
||||
// GitOutput runs `git [-C dir] args...` and returns its raw stdout.
|
||||
func GitOutput(ctx context.Context, dir string, args ...string) ([]byte, error) {
|
||||
func (i *Inspector) GitOutput(ctx context.Context, dir string, args ...string) ([]byte, error) {
|
||||
fullArgs := make([]string, 0, len(args)+2)
|
||||
if strings.TrimSpace(dir) != "" {
|
||||
fullArgs = append(fullArgs, "-C", dir)
|
||||
}
|
||||
fullArgs = append(fullArgs, args...)
|
||||
return HostCommandOutputFunc(ctx, "git", fullArgs...)
|
||||
return i.Runner(ctx, "git", fullArgs...)
|
||||
}
|
||||
|
||||
// GitTrimmedOutput returns GitOutput with surrounding whitespace trimmed.
|
||||
func GitTrimmedOutput(ctx context.Context, dir string, args ...string) (string, error) {
|
||||
output, err := GitOutput(ctx, dir, args...)
|
||||
func (i *Inspector) GitTrimmedOutput(ctx context.Context, dir string, args ...string) (string, error) {
|
||||
output, err := i.GitOutput(ctx, dir, args...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -396,8 +421,8 @@ func GitTrimmedOutput(ctx context.Context, dir string, args ...string) (string,
|
|||
}
|
||||
|
||||
// GitResolvedConfigValue reads git config key with --default "" --get.
|
||||
func GitResolvedConfigValue(ctx context.Context, dir, key string) (string, error) {
|
||||
return GitTrimmedOutput(ctx, dir, "config", "--default", "", "--get", key)
|
||||
func (i *Inspector) GitResolvedConfigValue(ctx context.Context, dir, key string) (string, error) {
|
||||
return i.GitTrimmedOutput(ctx, dir, "config", "--default", "", "--get", key)
|
||||
}
|
||||
|
||||
// ParseNullSeparatedOutput splits on NULs and trims, returning non-empty
|
||||
|
|
@ -415,10 +440,10 @@ func ParseNullSeparatedOutput(output []byte) []string {
|
|||
return values
|
||||
}
|
||||
|
||||
// RunHostCommand runs a host command via HostCommandOutputFunc, discarding
|
||||
// its stdout.
|
||||
func RunHostCommand(ctx context.Context, name string, args ...string) error {
|
||||
_, err := HostCommandOutputFunc(ctx, name, args...)
|
||||
// RunHostCommand runs a host command via the Inspector's Runner,
|
||||
// discarding its stdout.
|
||||
func (i *Inspector) RunHostCommand(ctx context.Context, name string, args ...string) error {
|
||||
_, err := i.Runner(ctx, name, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,8 @@ func seedRepo(t *testing.T) string {
|
|||
|
||||
func TestListOverlayPaths_TrackedOnlyByDefault(t *testing.T) {
|
||||
repo := seedRepo(t)
|
||||
got, err := ListOverlayPaths(context.Background(), repo, false)
|
||||
i := NewInspector()
|
||||
got, err := i.ListOverlayPaths(context.Background(), repo, false)
|
||||
if err != nil {
|
||||
t.Fatalf("ListOverlayPaths: %v", err)
|
||||
}
|
||||
|
|
@ -71,7 +72,8 @@ func TestListOverlayPaths_TrackedOnlyByDefault(t *testing.T) {
|
|||
|
||||
func TestListOverlayPaths_IncludeUntracked(t *testing.T) {
|
||||
repo := seedRepo(t)
|
||||
got, err := ListOverlayPaths(context.Background(), repo, true)
|
||||
i := NewInspector()
|
||||
got, err := i.ListOverlayPaths(context.Background(), repo, true)
|
||||
if err != nil {
|
||||
t.Fatalf("ListOverlayPaths: %v", err)
|
||||
}
|
||||
|
|
@ -89,7 +91,8 @@ func TestListOverlayPaths_IncludeUntracked(t *testing.T) {
|
|||
|
||||
func TestCountUntrackedPaths(t *testing.T) {
|
||||
repo := seedRepo(t)
|
||||
count, err := CountUntrackedPaths(context.Background(), repo)
|
||||
i := NewInspector()
|
||||
count, err := i.CountUntrackedPaths(context.Background(), repo)
|
||||
if err != nil {
|
||||
t.Fatalf("CountUntrackedPaths: %v", err)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue