stricttest v0.2.0 /go/hygiene
On this page

The Go env-hygiene package: a throwaway HOME and XDG dirs, an isolated git config and identity, transport lockdown, credential stripping, restoring chdir.

#go/hygiene

#go/hygiene

Package hygiene provides an always-on test-environment isolation floor for Go suites: a throwaway HOME, an isolated git config and identity, transport lockdown, credential stripping, and cleanup-restoring chdir.

#Usage

The composite entry point is [Isolate]. One call at the top of a test (or of a helper every test in the package funnels through) binds the whole floor:

func TestSomething(t *testing.T) { hygiene.Isolate(t) // HOME is a throwaway dir, git reads an empty global config with a // throwaway identity, only the file:// transport is allowed, and every // ambient credential variable is gone. }

Each piece is also exported on its own -- [ThrowawayHome], [IsolateGitConfig], [LockdownTransports], [StripCredentials] -- for suites that need one guarantee without the others. [Chdir] is separate on purpose: it is a per-test tool, not part of the floor.

#Contract

Every helper takes a [testing.TB], calls TB.Helper so failures point at the caller, and undoes itself through TB.Cleanup. Nothing here is global or process-wide: the isolation lives exactly as long as the test (or subtest) whose TB was passed in.

#Parallelism

Every environment mutation goes through TB.Setenv, which panics when the test has called T.Parallel. That is intended and is not worked around: a parallel test cannot own a process-wide variable like HOME, so a suite that wants this floor cannot run its tests in parallel with each other. The panic makes the conflict immediate and obvious instead of letting one test's HOME leak into another's.

#No socket guard

Unlike the Python plugin, this package ships no network guard. Go has no equivalent of sys.addaudithook, so there is no in-process interception point that could refuse a dial without patching the runtime. Network isolation for Go suites is owned by the sandbox runner (the bubblewrap wrapper that runs the suite with no network namespace), not by this package. Do not add a half-guard here that only covers net.Dial: a partial guard reads as a guarantee and is worse than none.

#GoPath

Go go
const GoPath KnownVar = iota

The closed preserve enum. Every entry names a cache or package location that holds build artifacts, not secrets, and that would otherwise send a toolchain into a cold rebuild (or hide an already-installed module) once HOME moves.

#GoModCache

Go go
const GoModCache

#GoCache

Go go
const GoCache

#PythonUserBase

Go go
const PythonUserBase

#CargoHome

Go go
const CargoHome

#RustupHome

Go go
const RustupHome

#NpmCache

Go go
const NpmCache

#UvCache

Go go
const UvCache

#PipCache

Go go
const PipCache

#GradleUserHome

Go go
const GradleUserHome

#CredentialVars

Go go
var CredentialVars = []string{

CredentialVars is the closed list of ambient credential vectors that [StripCredentials] removes from the environment. A test that genuinely needs one sets a FAKE value itself with TB.Setenv.

The list mirrors the Python plugin's CREDENTIAL_VARS, plus GIT_ASKPASS. That one variable is the two floors' single deliberate divergence: the Python floor pins it to /bin/false, while this package removes it here. Both close the same door -- git cannot obtain a credential either way -- and a cross-language test holds the rest of the two lists identical.

#Option

Go go
type Option func(*options)

Option customizes [Isolate]. The only option is [Preserve]; there is deliberately no option that turns a floor piece off.

#KnownVar

Go go
type KnownVar int

KnownVar names one toolchain cache variable that a suite may opt into preserving across the HOME repoint. The enum is closed: a caller cannot ask for an arbitrary variable name, so a credential vector can never become preservable by typo.

#Chdir

Go go
func Chdir(t testing.TB, dir string)

Chdir moves the process working directory to dir for the duration of t and restores the previous one when t finishes.

A failed restore FAILS the test rather than being logged and swallowed: the process is then sitting in the wrong directory, and every later test in the binary would run against it. A test that reports the damage is the only honest outcome.

The process working directory is global state, so this -- like the rest of the package -- is incompatible with T.Parallel. PWD is updated alongside the real working directory so that child processes inheriting the environment agree with the parent about where they are.

#ThrowawayHome

Go go
func ThrowawayHome(t testing.TB) string

ThrowawayHome repoints HOME, USERPROFILE and the four XDG base directories (XDG_CONFIG_HOME, XDG_DATA_HOME, XDG_CACHE_HOME, XDG_STATE_HOME) at a fresh temporary directory owned by t, and returns that directory. Every variable is restored when t finishes, and the directory is removed with the rest of t's TempDir tree.

The XDG directories are repointed as well as HOME because a great many tools read them FIRST: a suite that moved only HOME would still let a tool read the developer's real ~/.config (gh's hosts.yml lives there) and write into their real caches.

The call is memoized per TB: asking twice within the same test returns the same directory rather than moving HOME again. Each subtest gets its own TB and therefore its own home.

#IsolateGitConfig

Go go
func IsolateGitConfig(t testing.TB)

IsolateGitConfig cuts git off from the developer's configuration and identity: GIT_CONFIG_GLOBAL and GIT_CONFIG_SYSTEM point at empty files inside the throwaway home (which it allocates through [ThrowawayHome] if the test has not already), the author and committer identity become a throwaway one, and GIT_TERMINAL_PROMPT is 0 so no git invocation can ever block a test on a password prompt.

The config files are empty rather than carrying the identity, because the GIT_AUTHOR_ / GIT_COMMITTER_ variables below already supply it and outrank any config file -- a git invocation that ignores the config path entirely still cannot commit as the developer.

core.hooksPath is deliberately NOT set. It overrides repo-local hooks too, which would silently disable a suite's own pre-push-hook tests; an empty global config already prevents the developer's hooks from firing.

#LockdownTransports

Go go
func LockdownTransports(t testing.TB)

LockdownTransports restricts git to the local file transport for the duration of t. Any ssh://, https:// or git:// URL a test reaches for -- a real remote, a real fetch, a real push -- fails at the protocol check instead of touching the network.

GIT_SSH_COMMAND and GIT_PROXY_COMMAND are pinned to /bin/false as a second, independent layer. The protocol list is the first line and would be enough on its own, but a test (or a tool under test) that sets GIT_ALLOW_PROTOCOL itself would lift it -- and then the developer's real ssh, with their real key and their real proxy, is what git would run. Pinning both helpers means that path dies at an executable that only ever fails.

#StripCredentials

Go go
func StripCredentials(t testing.TB)

StripCredentials removes every variable in [CredentialVars] from the environment for the duration of t. The original values are restored when t finishes.

#Isolate

Go go
func Isolate(t testing.TB, opts ...Option)

Isolate binds the full environment floor for the duration of t: the preserved toolchain caches (if any) are pinned first, then HOME and the four XDG base directories are repointed at a throwaway directory, git's global and system config are emptied, the git identity is replaced, transports are locked down to file:// with git's ssh and proxy helpers pinned to a command that always fails, and every ambient credential variable is removed.

The throwaway home is not returned; call [ThrowawayHome] (which is memoized per TB and returns the same directory Isolate created) when the path is needed.

#Preserve

Go go
func Preserve(v ...KnownVar) Option

Preserve opts the named toolchain caches into surviving the HOME repoint. Each one is pinned to its current value, or to its default location under the real home when it is unset, before HOME changes.

#KnownVar.String

Go go
func (v KnownVar) String() string

String returns the enum member's Go name, or a marker for an out-of-range value.

#KnownVar.Env

Go go
func (v KnownVar) Env() string

Env returns the environment variable this enum member pins.

Search