1// Package spawntest helps write tests that use subprocesses. 2// 3// The subprocess runs a HTTP server on a UNIX domain socket, and the 4// test can make HTTP requests to control the behavior of the helper 5// subprocess. 6// 7// Helpers are identified by names they pass to Registry.Register. 8// This call should be placed in an init function. The test spawns the 9// subprocess by executing the same test binary in a subprocess, 10// passing it a special flag that is recognized by TestMain. 11// 12// This might get extracted to a standalone repository, if it proves 13// useful enough. 14package spawntest 15 16import ( 17 "context" 18 "errors" 19 "flag" 20 "io/ioutil" 21 "log" 22 "net" 23 "net/http" 24 "net/url" 25 "os" 26 "os/exec" 27 "path/filepath" 28 "sync" 29 "testing" 30 31 "bazil.org/fuse/fs/fstestutil/spawntest/httpjson" 32 "github.com/tv42/httpunix" 33) 34 35// Registry keeps track of helpers. 36// 37// The zero value is ready to use. 38type Registry struct { 39 mu sync.Mutex 40 helpers map[string]http.Handler 41 runName string 42 runHandler http.Handler 43} 44 45// Register a helper in the registry. 46// 47// This should be called from a top-level variable assignment. 48// 49// Register will panic if the name is already registered. 50func (r *Registry) Register(name string, h http.Handler) *Helper { 51 r.mu.Lock() 52 defer r.mu.Unlock() 53 if r.helpers == nil { 54 r.helpers = make(map[string]http.Handler) 55 } 56 if _, seen := r.helpers[name]; seen { 57 panic("spawntest: helper already registered: " + name) 58 } 59 r.helpers[name] = h 60 hh := &Helper{ 61 name: name, 62 } 63 return hh 64} 65 66type helperFlag struct { 67 r *Registry 68} 69 70var _ flag.Value = helperFlag{} 71 72func (hf helperFlag) String() string { 73 if hf.r == nil { 74 return "" 75 } 76 return hf.r.runName 77} 78 79func (hf helperFlag) Set(s string) error { 80 h, ok := hf.r.helpers[s] 81 if !ok { 82 return errors.New("helper not found") 83 } 84 hf.r.runName = s 85 hf.r.runHandler = h 86 return nil 87} 88 89const flagName = "spawntest.internal.helper" 90 91// AddFlag adds the command-line flag used to communicate between 92// Control and the helper to the flag set. Typically flag.CommandLine 93// is used, and this should be called from TestMain before flag.Parse. 94func (r *Registry) AddFlag(f *flag.FlagSet) { 95 v := helperFlag{r: r} 96 f.Var(v, flagName, "internal use only") 97} 98 99// RunIfNeeded passes execution to the helper if the right 100// command-line flag was seen. This should be called from TestMain 101// after flag.Parse. If running as the helper, the call will not 102// return. 103func (r *Registry) RunIfNeeded() { 104 h := r.runHandler 105 if h == nil { 106 return 107 } 108 f := os.NewFile(3, "<control-socket>") 109 l, err := net.FileListener(f) 110 if err != nil { 111 log.Fatalf("cannot listen: %v", err) 112 } 113 if err := http.Serve(l, h); err != nil { 114 log.Fatalf("http server error: %v", err) 115 } 116 os.Exit(0) 117} 118 119// Helper is the result of registering a helper. It can be used by 120// tests to spawn the helper. 121type Helper struct { 122 name string 123} 124 125type transportWithBase struct { 126 Base *url.URL 127 Transport http.RoundTripper 128} 129 130var _ http.RoundTripper = (*transportWithBase)(nil) 131 132func (t *transportWithBase) RoundTrip(req *http.Request) (*http.Response, error) { 133 ctx := req.Context() 134 req = req.Clone(ctx) 135 req.URL = t.Base.ResolveReference(req.URL) 136 return t.Transport.RoundTrip(req) 137} 138 139func makeHTTPClient(path string) *http.Client { 140 u := &httpunix.Transport{} 141 const loc = "helper" 142 u.RegisterLocation(loc, path) 143 t := &transportWithBase{ 144 Base: &url.URL{ 145 Scheme: httpunix.Scheme, 146 Host: loc, 147 }, 148 Transport: u, 149 } 150 client := &http.Client{ 151 Transport: t, 152 } 153 return client 154} 155 156// Spawn the helper. All errors will be reported via t.Logf and fatal 157// errors result in t.FailNow. The helper is killed after context 158// cancels. 159func (h *Helper) Spawn(ctx context.Context, t testing.TB) *Control { 160 executable, err := os.Executable() 161 if err != nil { 162 t.Fatalf("spawntest: cannot find our executable: %v", err) 163 } 164 165 // could use TB.TempDir() 166 // https://github.com/golang/go/issues/35998 167 dir, err := ioutil.TempDir("", "spawntest") 168 if err != nil { 169 t.Fatalf("spawnmount.Spawn: cannot make temp dir: %v", err) 170 } 171 defer func() { 172 if dir != "" { 173 if err := os.RemoveAll(dir); err != nil { 174 t.Logf("error cleaning temp dir: %v", err) 175 } 176 } 177 }() 178 179 controlPath := filepath.Join(dir, "control") 180 l, err := net.ListenUnix("unix", &net.UnixAddr{Name: controlPath, Net: "unix"}) 181 if err != nil { 182 t.Fatalf("cannot open listener: %v", err) 183 } 184 l.SetUnlinkOnClose(false) 185 defer l.Close() 186 187 lf, err := l.File() 188 if err != nil { 189 t.Fatalf("cannot get FD from listener: %v", err) 190 } 191 defer lf.Close() 192 193 cmd := exec.Command(executable, "-"+flagName+"="+h.name) 194 cmd.Stdout = os.Stdout 195 cmd.Stderr = os.Stderr 196 cmd.ExtraFiles = []*os.File{lf} 197 198 if err := cmd.Start(); err != nil { 199 t.Fatalf("spawntest: cannot start helper: %v", err) 200 } 201 defer func() { 202 if cmd != nil { 203 if err := cmd.Process.Kill(); err != nil { 204 t.Logf("error killing spawned helper: %v", err) 205 } 206 } 207 }() 208 209 c := &Control{ 210 t: t, 211 dir: dir, 212 cmd: cmd, 213 http: makeHTTPClient(controlPath), 214 } 215 dir = "" 216 cmd = nil 217 return c 218} 219 220// Control an instance of a helper running as a subprocess. 221type Control struct { 222 t testing.TB 223 dir string 224 cmd *exec.Cmd 225 http *http.Client 226} 227 228// Close kills the helper and frees resources. 229func (c *Control) Close() { 230 if c.cmd.ProcessState == nil { 231 // not yet Waited on 232 c.cmd.Process.Kill() 233 _ = c.cmd.Wait() 234 } 235 236 if c.dir != "" { 237 if err := os.RemoveAll(c.dir); err != nil { 238 c.t.Logf("error cleaning temp dir: %v", err) 239 } 240 c.dir = "" 241 } 242} 243 244// Signal send a signal to the helper process. 245func (c *Control) Signal(sig os.Signal) error { 246 return c.cmd.Process.Signal(sig) 247} 248 249// HTTP returns a HTTP client that can be used to communicate with the 250// helper. URLs passed to this helper should not include scheme or 251// host. 252func (c *Control) HTTP() *http.Client { 253 return c.http 254} 255 256// JSON returns a helper to make HTTP requests that pass data as JSON 257// to the resource identified by path. Path should not include scheme 258// or host. Path can be empty to communicate with the root resource. 259func (c *Control) JSON(path string) *httpjson.Resource { 260 return httpjson.JSON(c.http, path) 261} 262