-
Notifications
You must be signed in to change notification settings - Fork 28
/
v23test.go
419 lines (372 loc) · 11.5 KB
/
v23test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package v23test defines Shell, a wrapper around gosh.Shell that provides
// Vanadium-specific functionality such as credentials management,
// StartRootMountTable, and StartSyncbase.
package v23test
import (
"errors"
"flag"
"fmt"
"io/ioutil"
"os"
"runtime"
"strings"
"syscall"
"testing"
"time"
v23 "v.io/v23"
"v.io/v23/context"
"v.io/v23/rpc"
"v.io/v23/security"
"v.io/x/lib/envvar"
"v.io/x/lib/gosh"
"v.io/x/ref"
"v.io/x/ref/test"
"v.io/x/ref/test/expect"
)
const (
envChildOutputDir = "TMPDIR"
envShellTestProcess = "V23_SHELL_TEST_PROCESS"
)
var envPrincipal string
var (
errDidNotCallInitMain = errors.New("v23test: did not call v23test.TestMain or v23test.InitMain")
)
func init() {
envPrincipal = ref.EnvCredentials
}
// TODO(sadovsky):
// - Eliminate test.V23Init() and either add v23test.Init() or have v23.Init()
// check for an env var and perform test-specific configuration.
// - Switch to using the testing package's -test.short flag and eliminate
// SkipUnlessRunningIntegrationTests, the -v23.tests flag, and the "jiri test"
// implementation that parses test code to identify integration tests.
// Cmd wraps gosh.Cmd and provides Vanadium-specific functionality.
type Cmd struct {
*gosh.Cmd
S *expect.Session
sh *Shell
}
// Clone returns a new Cmd with a copy of this Cmd's configuration.
func (c *Cmd) Clone() *Cmd {
res := &Cmd{Cmd: c.Cmd.Clone(), sh: c.sh}
initSession(c.sh.tb, res)
return res
}
// WithCredentials returns a clone of this command, configured to use the given
// credentials.
func (c *Cmd) WithCredentials(cr *Credentials) *Cmd {
res := c.Clone()
res.Vars[envPrincipal] = cr.Handle
return res
}
// Shell wraps gosh.Shell and provides Vanadium-specific functionality.
type Shell struct {
*gosh.Shell
Ctx *context.T
tb testing.TB
pm principalManager
}
// NewShell creates a new Shell. Tests and benchmarks should pass their
// testing.TB; non-tests should pass nil. Ctx is the Vanadium context to use; if
// it's nil, NewShell will call v23.Init to create a context.
func NewShell(tb testing.TB, ctx *context.T) *Shell {
sh := &Shell{
Shell: gosh.NewShell(tb),
tb: tb,
}
if sh.Err != nil {
return sh
}
sh.ChildOutputDir = os.Getenv(envChildOutputDir)
// Filter out any v23test or credentials-related env vars coming from outside.
// Note, we intentionally retain envChildOutputDir ("TMPDIR") and
// envShellTestProcess, as these should be propagated downstream.
if envChildOutputDir != "TMPDIR" { // sanity check
panic(envChildOutputDir)
}
for _, key := range []string{ref.EnvCredentials} {
delete(sh.Vars, key)
}
if sh.tb != nil {
sh.Vars[envShellTestProcess] = "1"
}
cleanup := true
defer func() {
if cleanup {
sh.Cleanup()
}
}()
if err := sh.initPrincipalManager(); err != nil {
if _, ok := err.(errAlreadyHandled); !ok {
sh.handleError(err)
}
return sh
}
if err := sh.initCtx(ctx); err != nil {
if _, ok := err.(errAlreadyHandled); !ok {
sh.handleError(err)
}
return sh
}
cleanup = false
return sh
}
type errAlreadyHandled struct {
error
}
// ForkCredentials creates a new Credentials (with a fresh principal) and
// blesses it with the given extensions and no caveats, using this principal's
// default blessings. Additionally, it calls SetDefaultBlessings.
func (sh *Shell) ForkCredentials(extensions ...string) *Credentials {
return sh.ForkCredentialsFromPrincipal(v23.GetPrincipal(sh.Ctx), extensions...)
}
// ForkCredentialsFromPrincipal creates a new Credentials (with a fresh principal) and
// blesses it with the given extensions and no caveats, using the given principal's
// default blessings. Additionally, it calls SetDefaultBlessings.
func (sh *Shell) ForkCredentialsFromPrincipal(principal security.Principal, extensions ...string) *Credentials {
sh.Ok()
creds, err := newCredentials(sh.pm)
if err != nil {
sh.handleError(err)
return nil
}
if err := addDefaultBlessings(principal, creds.Principal, extensions...); err != nil {
sh.handleError(err)
return nil
}
return creds
}
// ForkContext creates a new context with forked credentials.
func (sh *Shell) ForkContext(extensions ...string) *context.T {
sh.Ok()
c := sh.ForkCredentials(extensions...)
if sh.Err != nil {
return nil
}
ctx, err := v23.WithPrincipal(sh.Ctx, c.Principal)
sh.handleError(err)
return ctx
}
// Cleanup cleans up all resources associated with this Shell.
// See gosh.Shell.Cleanup for detailed description.
func (sh *Shell) Cleanup() {
// Run sh.Shell.Cleanup even if DebugSystemShell panics.
defer sh.Shell.Cleanup()
if sh.tb != nil && sh.tb.Failed() && test.IntegrationTestsDebugShellOnError {
sh.DebugSystemShell()
}
}
// binDir is the directory where BuildGoPkg writes binaries. Initialized by
// InitMain.
var binDir string
// BuildGoPkg compiles a Go package using the "go build" command and writes the
// resulting binary to a temporary directory, or to the -o flag location if
// specified. If -o is relative, it is interpreted as relative to the temporary
// directory. If the binary already exists at the target location, it is not
// rebuilt. Returns the absolute path to the binary.
func BuildGoPkg(sh *Shell, pkg string, flags ...string) string {
sh.Ok()
if !calledInitMain {
sh.handleError(errDidNotCallInitMain)
return ""
}
return gosh.BuildGoPkg(sh.Shell, binDir, pkg, flags...)
}
var calledInitMain = false
// InitMain is called by v23test.TestMain; non-tests must call it early on in
// main(), before flags are parsed. It calls gosh.InitMain, initializes the
// directory used by v23test.BuildGoPkg, and returns a cleanup function.
//
// InitMain can also be used by test developers with complex setup or teardown
// requirements, where v23test.TestMain is unsuitable. InitMain must be called
// early on in TestMain, before m.Run is called. The returned cleanup function
// should be called after m.Run but before os.Exit.
func InitMain() func() {
if calledInitMain {
panic("v23test: already called v23test.TestMain or v23test.InitMain")
}
calledInitMain = true
gosh.InitMain()
var err error
binDir, err = ioutil.TempDir("", "bin-")
if err != nil {
panic(err)
}
return func() {
os.RemoveAll(binDir)
}
}
// TestMain calls flag.Parse and does some v23test/gosh setup work, then calls
// os.Exit(m.Run()). Developers with complex setup or teardown requirements may
// need to use InitMain instead.
func TestMain(m *testing.M) {
flag.Parse()
var code int
func() {
defer InitMain()()
code = m.Run()
}()
os.Exit(code)
}
// SkipUnlessRunningIntegrationTests should be called first thing inside of the
// test function body of an integration test. It causes this test to be skipped
// unless integration tests are being run, i.e. unless the -v23.tests flag is
// set.
// TODO(sadovsky): Switch to using -test.short. See TODO above.
func SkipUnlessRunningIntegrationTests(tb testing.TB) {
// Note: The "jiri test run vanadium-integration-test" command looks for test
// function names that start with "TestV23", and runs "go test" for only those
// packages containing at least one such test. That's how it avoids passing
// the -v23.tests flag to packages for which the flag is not registered.
name, err := callerName()
if err != nil {
tb.Fatal(err)
}
if !strings.HasPrefix(name, "TestV23") {
tb.Fatalf("integration test names must start with \"TestV23\": %s", name)
return
}
if !test.IntegrationTestsEnabled {
tb.SkipNow()
}
}
// Methods for starting subprocesses
// =================================
func initSession(tb testing.TB, c *Cmd) {
c.S = expect.NewSession(tb, c.StdoutPipe(), time.Minute)
c.S.SetVerbosity(testing.Verbose())
c.S.SetContinueOnError(c.sh.ContinueOnError)
}
func newCmd(sh *Shell, c *gosh.Cmd) *Cmd {
res := &Cmd{Cmd: c, sh: sh}
initSession(sh.tb, res)
res.Vars[envPrincipal] = sh.ForkCredentials("child").Handle
return res
}
// Cmd returns a Cmd for an invocation of the named program. The given arguments
// are passed to the child as command-line arguments.
func (sh *Shell) Cmd(name string, args ...string) *Cmd {
c := sh.Shell.Cmd(name, args...)
if sh.Err != nil {
return nil
}
return newCmd(sh, c)
}
// FuncCmd returns a Cmd for an invocation of the given registered Func. The
// given arguments are gob-encoded in the parent process, then gob-decoded in
// the child and passed to the Func as parameters. To specify command-line
// arguments for the child invocation, append to the returned Cmd's Args.
func (sh *Shell) FuncCmd(f *gosh.Func, args ...interface{}) *Cmd {
sh.Ok()
if !calledInitMain {
sh.handleError(errDidNotCallInitMain)
return nil
}
c := sh.Shell.FuncCmd(f, args...)
if sh.Err != nil {
return nil
}
return newCmd(sh, c)
}
// DebugSystemShell
// ================
// DebugSystemShell drops the user into a debug system shell (e.g. bash) that
// includes all environment variables from sh. If there is no controlling TTY,
// DebugSystemShell does nothing.
func (sh *Shell) DebugSystemShell() {
cwd, err := os.Getwd()
if err != nil {
sh.tb.Fatalf("Getwd() failed: %v\n", err)
return
}
// Transfer stdin, stdout, and stderr to the new process, and set target
// directory for the system shell to start in.
devtty := "/dev/tty"
fd, err := syscall.Open(devtty, syscall.O_RDWR, 0)
if err != nil {
sh.tb.Logf("WARNING: Open(%q) failed: %v\n", devtty, err)
return
}
file := os.NewFile(uintptr(fd), devtty)
attr := os.ProcAttr{
Files: []*os.File{file, file, file},
Dir: cwd,
}
env := envvar.MergeMaps(envvar.SliceToMap(os.Environ()), sh.Vars)
env[envPrincipal] = sh.ForkCredentials("debug").Handle
attr.Env = envvar.MapToSlice(env)
write := func(s string) {
if _, err := file.WriteString(s); err != nil {
sh.tb.Fatalf("WriteString(%q) failed: %v\n", s, err)
return
}
}
write(">> Starting a new interactive shell\n")
write(">> Hit Ctrl-D to resume the test\n")
shellPath := "/bin/sh"
if shellPathFromEnv := os.Getenv("SHELL"); shellPathFromEnv != "" {
shellPath = shellPathFromEnv
}
proc, err := os.StartProcess(shellPath, []string{}, &attr)
if err != nil {
sh.tb.Fatalf("StartProcess(%q) failed: %v\n", shellPath, err)
return
}
// Wait until the user exits the shell.
state, err := proc.Wait()
if err != nil {
sh.tb.Fatalf("Wait() failed: %v\n", err)
return
}
write(fmt.Sprintf(">> Exited shell: %s\n", state.String()))
}
// Internals
// =========
// handleError is intended for use by public Shell method implementations.
func (sh *Shell) handleError(err error) {
sh.HandleErrorWithSkip(err, 3)
}
func callerName() (string, error) {
pc, _, _, ok := runtime.Caller(2)
if !ok {
return "", errors.New("runtime.Caller failed")
}
name := runtime.FuncForPC(pc).Name()
// Strip package path.
return name[strings.LastIndex(name, ".")+1:], nil
}
func (sh *Shell) initPrincipalManager() error {
dir := sh.MakeTempDir()
if sh.Err != nil {
return errAlreadyHandled{sh.Err}
}
pm := newFilesystemPrincipalManager(dir)
sh.pm = pm
return nil
}
func (sh *Shell) initCtx(ctx *context.T) error {
if ctx == nil {
var shutdown func()
ctx, shutdown = v23.Init()
if sh.AddCleanupHandler(shutdown); sh.Err != nil {
return errAlreadyHandled{sh.Err}
}
if sh.tb != nil {
creds, err := newRootCredentials(sh.pm)
if err != nil {
return err
}
if ctx, err = v23.WithPrincipal(ctx, creds.Principal); err != nil {
return err
}
}
}
if sh.tb != nil {
ctx = v23.WithListenSpec(ctx, rpc.ListenSpec{Addrs: rpc.ListenAddrs{{Protocol: "tcp", Address: "127.0.0.1:0"}}})
}
sh.Ctx = ctx
return nil
}