Help us shape the future of Go! Take the 2026 Go Developer Survey

testenv

package
v0.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 28, 2026 License: Apache-2.0 Imports: 26 Imported by: 0

Documentation

Overview

Package testenv provides cross-platform process environment isolation for tests.

Index

Constants

View Source
const (
	TailnetDevicesPath  = "/api/v2/tailnet/-/devices"
	TailnetPolicyPath   = "/api/v2/tailnet/-/acl"
	TailnetSettingsPath = "/api/v2/tailnet/-/settings"
)
View Source
const DoctorSkipTailscaleSSHEnv = "TSLINK_DOCTOR_SKIP_TAILSCALE_SSH"

DoctorSkipTailscaleSSHEnv is the product knob that makes `tslink doctor` skip its read of the local tailscaled (cmd/doctor.go). Main sets it so every compiled tslink child a test runs inherits it; in-process tests fake the doctor seams instead, so the knob changes nothing inside the test binary.

View Source
const IsolationFailure = "testenv: cannot isolate this test binary"

IsolationFailure starts the message Main prints when it refuses to run a test binary it could not isolate.

View Source
const NetworkGuardReportEnv = "TSLINK_NETWORK_GUARD_REPORT"
View Source
const RootEnv = "TSLINK_TESTENV_ROOT"

RootEnv names the temporary root Main created for the running test binary. Every home, config, data and cache location of the process points inside it.

A test binary that starts with RootEnv naming a root whose marker a running test binary holds locked was started by a test in an already isolated binary (a helper re-exec such as `os.Args[0] -test.run=^TestX$`). Its TSLINK_ variables were put there by that test, not by the contributor's shell, so Main keeps them; it still moves every location into a fresh root of its own.

View Source
const RootPrefix = "tslink-testenv-"

RootPrefix is the name prefix of each root Main creates under os.TempDir().

View Source
const ServiceManagerGuardReportEnv = "TSLINK_SERVICE_MANAGER_GUARD_REPORT"

ServiceManagerGuardReportEnv makes the guard print its per-package summary line even when it recorded zero blocked attempts, so a green run can still prove the guard was installed rather than silently absent.

Accepted true values are 1, t, true, y, yes and on (case-insensitive). Anything else, including the empty string, is off. The spelling used to be an exact "1" comparison, which silently treated TSLINK_..._REPORT=true as off; a CI operator who asked for the audit trail and got silence had no way to tell that from "the guard is not installed".

View Source
const ServiceManagerShimExitCode = 97

ServiceManagerShimExitCode is the status the planted fakes exit with. It is outside the range tslink itself uses (0-5) and outside the shell's 126/127, so a test can tell "the shim refused" apart from both a real failure and a missing binary.

View Source
const ServiceManagerShimLogEnv = "TSLINK_TEST_SERVICE_MANAGER_SHIM_LOG"

ServiceManagerShimLogEnv names the file the planted fake service managers append their argv to. The shim bakes in a default path, so an invocation that inherits nothing still leaves a record; a test that wants its own record sets this on the child it spawns and reads that file instead.

View Source
const ServiceManagerShimStderrPrefix = "tslink-test-service-manager-shim:"

ServiceManagerShimStderrPrefix begins every line the planted fakes write to stderr. A child that prints this proves the resolution reached the shim rather than the real binary.

Variables

View Source
var ErrServiceManagerBlocked = errors.New("test service manager guard blocked a real OS service manager call")

ErrServiceManagerBlocked is returned in place of running the real service manager binary. It is a sentinel no real launchctl/systemctl invocation can ever produce, so a test can assert on it with errors.Is and know for certain that nothing was executed.

Why this guard exists: on 2026-09-16 a `go test` run of package cmd reached the real launchctl exit and ran `bootout`+`bootstrap` against the live gui/<uid> domain. launchd's domain namespace is global to the login session, so it is reachable from any process regardless of HOME or t.TempDir(); the job label is a compile-time constant shared with the installed daemon. The test therefore replaced the developer's own com.tslink.daemon job with one pointing at the ephemeral cmd.test binary, and when the temp dir was cleaned up launchd could no longer start it (exit 78, EX_CONFIG). That daemon stayed down for 23 minutes. Path isolation cannot prevent this; only closing the process exit can.

View Source
var ErrUnfakedHostSeam = errors.New("a test reached a production seam that touches a real host resource without faking it")

ErrUnfakedHostSeam is returned by a host seam's test-binary default. A production seam that reaches a real host resource (a tsnet node, the local tailscaled, a browser) is replaced in its package's TestMain by a default that returns this error instead; a test that exercises the seam installs its own fake.

Functions

func AllowRealServiceManagerInChildProcesses

func AllowRealServiceManagerInChildProcesses(reason string) (func(), error)

AllowRealServiceManagerInChildProcesses takes the planted shim directory back out of PATH, so that a test which must drive the caller's real service manager reaches it instead of a fake that answers ServiceManagerShimExitCode for everything.

It exists because the shim's protection is not selective: it rewrites this process's PATH once, and every child that inherits the environment resolves the manager to a fake. That is the right default and it is also wrong for exactly one test in this repo -- the systemd install e2e, which is gated behind TSLINK_SYSTEMD_E2E=1, refuses to run against anything that looks like a real installation, and exists precisely to drive real systemd inside a disposable VM. Without an opt-out that test does not fail; it silently verifies the shim.

The opt-out is deliberately loud and narrow:

  • it requires a reason, and the reason is printed in the guard's teardown report alongside the seam-level AllowReal opt-ins, so a run that took it cannot look like a run that did not;
  • it fails rather than degrading when no shim is installed, because a caller asking for this has decided the fakes are in its way, and quietly returning a no-op would leave it believing it got what it asked for;
  • it only moves PATH. The in-process seams stay blocked; reaching those still requires ServiceManagerGuard.AllowReal.

Call the returned function to put PATH back.

func ConfigDir

func ConfigDir(home string) string

ConfigDir returns the non-empty TSLink config override associated with home. It deliberately does not require the directory to exist yet.

func HomeEnv

func HomeEnv(home string) [][2]string

HomeEnv returns the variable assignments that make home the only home of a process: the conventional home variables of every platform, the platform config/data/cache/state directories derived from them, and TSLink's config override. Main and SetHome both apply exactly this set.

func Main

func Main(m *testing.M, run func() int) int

Main is the TestMain entry point of every test binary in this module. It isolates the process before any test runs, calls run (m.Run when nil), and returns the exit code for os.Exit:

  • every TSLINK_ variable inherited from the contributor's environment is removed, so an exported credential or knob can neither be consumed by a test nor change a result; a test that needs one sets it with t.Setenv;
  • HOME, USERPROFILE, APPDATA, LOCALAPPDATA, XDG_CONFIG_HOME, XDG_DATA_HOME, XDG_CACHE_HOME, XDG_STATE_HOME and TSLINK_CONFIG_DIR point into one fresh temporary root, so os.UserHomeDir, os.UserConfigDir, os.UserCacheDir and config.Dir resolve there, in this process and in every child it starts;
  • DoctorSkipTailscaleSSHEnv is set for compiled tslink children;
  • a call to a host seam's test-binary default (UnfakedHostSeam) turns the package red with the call's stack;
  • the root is removed after run returns, and roots that interrupted, timed-out or crashed binaries left in os.TempDir() are removed before the new one is made.

Everything that may still reach a real host resource is an explicit opt-in inside a test: t.Setenv, a fake that delegates to the real seam, or RealHostMain for a whole binary.

func PlantServiceManagerShims

func PlantServiceManagerShims(dir string) (logPath string, planted []string, err error)

PlantServiceManagerShims writes one executable fake per service manager into dir, creates the log the fakes append to, and returns the log path together with the names it actually planted. The caller owns dir.

The fakes are /bin/sh scripts rather than compiled binaries so that planting them costs no build and cannot itself fail for toolchain reasons. On Windows they are not planted at all: none of these binaries exists there, and a shell script would not be executable anyway. That is stated here rather than hidden behind a nil return, because "the shim protects nothing on Windows" is a fact about the mechanism and not a detail.

func RealHostMain

func RealHostMain(m *testing.M, reason string, run func() int) int

RealHostMain runs a test binary without Main's isolation. It exists for the one gated test that must reach real host state (the Linux systemd E2E, which cmd's TestMain admits only with TSLINK_SYSTEMD_E2E=1 and one exact -test.run selector on a disposable VM). reason is printed so the run's log records the opt-in. Unfaked host seams are still reported.

func Root

func Root() string

Root returns the root Main created for this test binary, or "" when the binary is not running under Main. Before Main has run, a non-empty Root means the binary was started by a test of an isolated binary: helper modes that a TestMain dispatches before calling Main check it, so a variable a contributor exported cannot turn a top-level run into a helper.

Once Main has made this binary's root, Root reports it without opening its marker. This process holds that marker's lock, and only local flock promises that a second open file of it is a separate lock holder. Where Linux emulates flock with byte-range locks (NFS, SMB/CIFS), such a probe can be refused, and Root would report "", or be granted to the process that already holds the lock, and its unlock and close would then drop the lock while the root is in use, so another binary's reclaimStaleRoots could remove it.

func RunWithNonLoopbackDialGuard

func RunWithNonLoopbackDialGuard(run func() int, packageName string) int

RunWithNonLoopbackDialGuard runs one package's complete test binary while intercepting the real TCP dial hook used by http.DefaultTransport. Loopback dials remain available for httptest servers; every non-loopback attempt is blocked, recorded, and turns an otherwise-passing package into a failure.

func RunWithServiceManagerGuard

func RunWithServiceManagerGuard(run func() int, packageName string, seams ...ServiceManagerSeam) (code int)

RunWithServiceManagerGuard runs one package's complete test binary with every listed OS service manager exit closed. See ErrServiceManagerBlocked for the incident this prevents.

Teardown is deferred so a panic in run() still restores the seams and still emits the report: the blocked-attempt list is the only record of what went wrong, and losing it to a panic means the next reader sees a crash with no explanation.

func SetHome

func SetHome(t testing.TB, home string) string

SetHome points every home, config, data and cache lookup of the process at home (the same variable set Main uses, see HomeEnv) and returns TSLink's config override. The override is always non-empty and does not need to exist yet; testing.TB.Setenv restores the prior environment exactly during cleanup.

func TailnetDevicePath

func TailnetDevicePath(nodeID string) string

TailnetDevicePath returns the exact REST path used by the v2 SDK for one device. Tests should pass a NodeID, matching TSLink's deletion contract.

func UnfakedHostSeam

func UnfakedHostSeam(seam string) error

UnfakedHostSeam records that the named seam ran its test-binary default and returns an error that names it. Main prints every recorded call with its stack after the tests finish and fails the package, so a forgotten fake is loud even when the product code turns the error into a soft outcome.

Types

type ServiceManagerAttempt

type ServiceManagerAttempt struct {
	Manager string
	Args    []string
	Stack   []byte
}

ServiceManagerAttempt records one blocked call.

type ServiceManagerCall

type ServiceManagerCall func(args ...string) ([]byte, error)

ServiceManagerCall is one function-shaped process exit to a service manager.

type ServiceManagerGuard

type ServiceManagerGuard struct {
	// contains filtered or unexported fields
}

ServiceManagerGuard intercepts service-manager process exits for the lifetime of one test binary. The default state is deny: a seam that no test stubbed returns ErrServiceManagerBlocked without executing anything, the call site is recorded with its stack, and the package exits non-zero.

func ActiveServiceManagerGuard

func ActiveServiceManagerGuard() *ServiceManagerGuard

ActiveServiceManagerGuard returns the guard installed by the running RunWithServiceManagerGuard, or nil when no guard is installed. A package whose TestMain claims to be guarded can assert on this being non-nil.

func NewServiceManagerGuard

func NewServiceManagerGuard(packageName string) *ServiceManagerGuard

NewServiceManagerGuard builds an installed-nowhere guard. Callers wire seams with Install; tests construct guards directly to exercise the guard itself.

func (*ServiceManagerGuard) AllowReal

func (g *ServiceManagerGuard) AllowReal(manager, reason string) func()

AllowReal is the only way for a guarded seam to reach the real binary: forgetting to stub a seam cannot silently fall through to it, and every opt-in is named in the guard's report.

It is not a claim that the real binary is otherwise unreachable from a test process. A test can still build its own exec.Command, or re-point the seam variable at one (see DriftedSeams and the source inventory test for the two checks that cover those). The returned function revokes the opt-in.

func (*ServiceManagerGuard) Attempts

Attempts returns the blocked calls recorded so far.

func (*ServiceManagerGuard) Blocked

Blocked returns the replacement call for manager, remembering real so an explicit AllowReal opt-in can delegate to it later.

func (*ServiceManagerGuard) DriftedSeams

func (g *ServiceManagerGuard) DriftedSeams() []string

DriftedSeams names every seam that is no longer pointing at guard code.

This is the commission half of the guard. Default-deny closes the omission case (a test that forgot to stub), but nothing stops a test from writing

launchctlCombinedOutput = func(args ...string) ([]byte, error) {
        return exec.Command(theRealBinary, args...).CombinedOutput()
}

and never putting it back: that goes *through* the seam variable, so the guard's own closure is simply gone. Comparing the seam's current code pointer against the closure Install put there turns that into a red run.

What it cannot see, stated plainly:

  • a seam that is re-pointed and then restored before this runs. Every closure Blocked returns shares one code pointer (they come from the same func literal), so "restored to a guard closure" is the only property checked, not "restored to *my* guard closure".
  • anything that happens after the check: a leaked goroutine, or the window between restore() and process exit.
  • a test that never touches the seam and calls exec.Command itself. That is the source inventory's job, not this one.

func (*ServiceManagerGuard) Install

func (g *ServiceManagerGuard) Install(seams ...ServiceManagerSeam) func()

Install replaces every seam with its blocking counterpart and returns a restore function that puts the real implementations back.

A malformed seam panics rather than being skipped. A seam with a nil Get or Set protects nothing, and the old silent `continue` produced a guard that looked installed and reported nothing while one exit stayed wide open.

func (*ServiceManagerGuard) Report

func (g *ServiceManagerGuard) Report() bool

Report writes the guard summary to stderr and reports whether the package must fail. A blocked attempt is always fatal for the package: the test that made it is not isolated, and the next run of it on a developer machine is what takes that developer's own installed daemon down.

An opt-in line is printed whenever one was taken, independent of the env var and of whether anything was blocked. An AllowReal that runs the real binary and blocks nothing used to leave no trace at all unless the operator had already set the report env, i.e. the audit record vanished in exactly the environment (CI) where nobody is watching. A run with no opt-ins and no blocked attempts stays silent unless the env asks.

func (*ServiceManagerGuard) ServiceManagerShim

func (g *ServiceManagerGuard) ServiceManagerShim() *ServiceManagerPathShim

ServiceManagerShim returns the child-process PATH shim this guard installed, or nil when none was installed. A test uses it to find the default log and to point a child at a log of its own.

type ServiceManagerPathShim

type ServiceManagerPathShim struct {
	// Dir is the directory put at the front of PATH. It is set even on a
	// platform where nothing was planted, because the log lives there too.
	Dir string
	// LogPath is the file the fakes append to when the child inherits no
	// override for ServiceManagerShimLogEnv.
	LogPath string
	// Planted names the fakes that actually exist in Dir. It is empty on
	// Windows, where the shim protects nothing; see PlantServiceManagerShims.
	//
	// It is a list rather than a bool because the teardown report has to say
	// which of "no child called a manager" and "no fake was there to call"
	// produced a count of zero. Those two read identically otherwise, and the
	// second is the one that means the package ran unprotected.
	Planted []string
}

ServiceManagerPathShim is a directory of fake service manager binaries plus the file they record into.

func InstallServiceManagerPathShim

func InstallServiceManagerPathShim() (*ServiceManagerPathShim, func(), error)

InstallServiceManagerPathShim plants the fakes in a fresh temporary directory and puts that directory at the front of this process's PATH.

Why PATH and not a helper every spawn site has to call: production resolves its service manager by bare name (exec.Command("launchctl", ...)), and Go resolves a bare name against the *spawning* process's PATH. A child that inherits this process's environment therefore lands on the fake, and so does its own children. Inheriting is the default for exec.Cmd (a nil Env) and for every helper in this repo (append(os.Environ(), ...)), so the protected path is the one a spawn site gets by doing nothing. There is no call to forget.

Bypassing it requires a deliberate act that is visible in source: building cmd.Env from scratch without os.Environ(), overwriting PATH afterwards, or naming the binary by absolute path. The source scan in service_manager_exit_inventory_test.go is what covers those.

The returned restore function puts PATH back and removes the planted directory; read Calls() before calling it.

func (*ServiceManagerPathShim) Calls

Calls parses this shim's log.

type ServiceManagerSeam

type ServiceManagerSeam struct {
	Manager string
	Get     func() ServiceManagerCall
	Set     func(ServiceManagerCall)
}

ServiceManagerSeam binds one package-level seam variable to the guard. Get captures the real implementation so an explicitly opted-in test can still reach it; Set installs the blocking replacement.

type ServiceManagerShimCall

type ServiceManagerShimCall struct {
	Manager string
	Args    []string
}

ServiceManagerShimCall is one recorded invocation of a planted fake.

func ReadServiceManagerShimCalls

func ReadServiceManagerShimCalls(logPath string) ([]ServiceManagerShimCall, error)

ReadServiceManagerShimCalls parses a shim log into calls.

func (ServiceManagerShimCall) IsReadOnly

func (c ServiceManagerShimCall) IsReadOnly() bool

IsReadOnly reports whether the call could not have changed system state.

func (ServiceManagerShimCall) String

func (c ServiceManagerShimCall) String() string

String renders the call the way it was typed.

func (ServiceManagerShimCall) Verb

func (c ServiceManagerShimCall) Verb() string

Verb is the first non-flag argument, which is what decides whether the call would have changed system state. An invocation with no such argument returns the empty string and is treated as unknown, i.e. not read-only.

type StatefulTailnet

type StatefulTailnet struct {
	// contains filtered or unexported fields
}

StatefulTailnet is an in-memory httptest implementation of the Tailscale v2 REST resources TSLink currently consumes: devices, policy file, and tailnet settings. Its wire payloads use the v2.9.0 SDK types and JSON field names.

func NewStatefulTailnet

func NewStatefulTailnet(t testing.TB) *StatefulTailnet

NewStatefulTailnet starts an isolated loopback server with an empty policy.

func (*StatefulTailnet) Client

func (f *StatefulTailnet) Client(apiKey string) *tailscale.Client

Client returns the real v2 SDK client configured for this fake.

func (*StatefulTailnet) DelayNext

func (f *StatefulTailnet) DelayNext(method, path string, delay time.Duration)

DelayNext delays the next exact method/path request. A caller with a shorter context deadline observes a real HTTP timeout/cancellation.

func (*StatefulTailnet) Devices

func (f *StatefulTailnet) Devices() []tailscale.Device

Devices returns an isolated snapshot of current device state.

func (*StatefulTailnet) FailNext

func (f *StatefulTailnet) FailNext(method, path string, status int)

FailNext makes the next exact method/path request return an API error.

func (*StatefulTailnet) Policy

func (f *StatefulTailnet) Policy() (string, string)

Policy returns the raw policy and its current HTTP ETag value.

func (*StatefulTailnet) Requests

func (f *StatefulTailnet) Requests() []TailnetRequest

Requests returns recorded non-secret request metadata.

func (*StatefulTailnet) SetDevices

func (f *StatefulTailnet) SetDevices(devices []tailscale.Device)

SetDevices replaces the fake tailnet's device state.

func (*StatefulTailnet) SetPolicy

func (f *StatefulTailnet) SetPolicy(policy string)

SetPolicy replaces the raw HuJSON policy and advances its ETag.

func (*StatefulTailnet) SetTailnetSettings

func (f *StatefulTailnet) SetTailnetSettings(settings tailscale.TailnetSettings)

SetTailnetSettings replaces the read-only settings response.

func (*StatefulTailnet) URL

func (f *StatefulTailnet) URL() string

URL returns the loopback-only base URL accepted by tailapi.APIBaseURLEnv.

type TailnetRequest

type TailnetRequest struct {
	Method      string
	Path        string
	Accept      string
	ContentType string
	IfMatch     string
	BasicAuth   bool
}

TailnetRequest records only non-secret request metadata. Authorization and request bodies are deliberately excluded.

Directories

Path Synopsis
Package localapitest builds Tailscale LocalAPI clients for tests.
Package localapitest builds Tailscale LocalAPI clients for tests.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL