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