| package exec |
| |
| import ( |
| "fmt" |
| "io" |
| "log" |
| "os" |
| "os/exec" |
| "sync" |
| "syscall" |
| "testing" |
| "time" |
| |
| // Use mock timekeeper to avoid actually sleeping during the test. |
| "veyron/runtimes/google/testing/timekeeper" |
| ) |
| |
| // We always expect there to be exactly three open file descriptors when the |
| // test starts out: STDIN, STDOUT, and STDERR. This assumption is tested |
| // in init below, and in the rare cases where it's wrong, we bail out. |
| const baselineOpenFiles = 3 |
| |
| func init() { |
| if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" { |
| return |
| } |
| if want, got := baselineOpenFiles, openFiles(); want != got { |
| panic(fmt.Errorf("Test expected to start with %d open files, found %d instead.\nThis can happen if parent process has any open file descriptors, e.g. pipes, that are being inherited.", want, got)) |
| } |
| } |
| |
| // These tests need to run a subprocess and we reuse this same test binary |
| // to do so. A fake test 'TestHelperProcess' contains the code we need to |
| // run in the child and we simply run this same binary with a test.run= arg |
| // that runs just that test. This idea was taken from the tests for os/exec. |
| func helperCommand(s ...string) *exec.Cmd { |
| cs := []string{"-test.run=TestHelperProcess", "--"} |
| cs = append(cs, s...) |
| cmd := exec.Command(os.Args[0], cs...) |
| cmd.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...) |
| return cmd |
| } |
| |
| // noChild returns true if the child process doesn't exist |
| func noChild(p *ParentHandle) bool { |
| err := syscall.Kill(p.c.Process.Pid, 0) |
| return err != nil |
| } |
| |
| func goroutinesDone(t *testing.T, p *ParentHandle) { |
| p.doneStatus.Wait() |
| p.doneWait.Wait() |
| } |
| |
| func openFiles() int { |
| f, err := os.Open("/dev/null") |
| if err != nil { |
| panic("Failed to open /dev/null\n") |
| } |
| n := f.Fd() |
| f.Close() |
| return int(n) |
| } |
| |
| func clean(t *testing.T, ph ...*ParentHandle) { |
| for _, p := range ph { |
| alreadyClean := noChild(p) |
| p.Clean() |
| if !alreadyClean && !noChild(p) { |
| t.Errorf("child process left behind even after calling Clean") |
| } |
| goroutinesDone(t, p) |
| } |
| if want, got := baselineOpenFiles, openFiles(); want != got { |
| t.Errorf("Leaking file descriptors: expect %d, got %d", want, got) |
| } |
| } |
| |
| func read(ch chan bool, r io.Reader, m string) { |
| buf := make([]byte, 4096*4) |
| n, err := r.Read(buf) |
| if err != nil { |
| log.Printf("failed to read message: error %s, expecting '%s'\n", |
| err, m) |
| ch <- false |
| return |
| } |
| g := string(buf[:n]) |
| b := g == m |
| if !b { |
| log.Printf("read '%s', not '%s'\n", g, m) |
| } |
| ch <- b |
| } |
| |
| func expectMessage(r io.Reader, m string) bool { |
| ch := make(chan bool, 1) |
| go read(ch, r, m) |
| select { |
| case b := <-ch: |
| return b |
| case <-time.After(5 * time.Second): |
| log.Printf("expectMessage: timeout\n") |
| return false |
| } |
| panic("unreachable") |
| } |
| |
| func TestAuthExchange(t *testing.T) { |
| cmd := helperCommand("testAuth") |
| stderr, _ := cmd.StderrPipe() |
| ph := NewParentHandle(cmd, "dummy_secret") |
| err := ph.Start() |
| if err != nil { |
| t.Fatalf("testAuthTest: start: %v", err) |
| } |
| if !expectMessage(stderr, "dummy_secret") { |
| t.Errorf("unexpected output from child") |
| } else { |
| if err = cmd.Wait(); err != nil { |
| t.Errorf("testAuthTest: wait: %v", err) |
| } |
| } |
| clean(t, ph) |
| } |
| |
| func TestNoVersion(t *testing.T) { |
| // Make sure that Init correctly tests for the presence of VEXEC_VERSION |
| _, err := NewChildHandle() |
| if err != ErrNoVersion { |
| t.Errorf("Should be missing Version") |
| } |
| } |
| |
| func waitForReady(t *testing.T, cmd *exec.Cmd, name string, delay int, ph *ParentHandle) error { |
| err := ph.Start() |
| if err != nil { |
| t.Fatalf("%s: start: %v", name, err) |
| return err |
| } |
| return ph.WaitForReady(time.Duration(delay) * time.Second) |
| } |
| |
| func readyHelperCmd(t *testing.T, cmd *exec.Cmd, name, result string) *ParentHandle { |
| stderr, err := cmd.StderrPipe() |
| if err != nil { |
| t.Fatalf("%s: failed to get stderr pipe\n", name) |
| } |
| ph := NewParentHandle(cmd, "") |
| if err := waitForReady(t, cmd, name, 4, ph); err != nil { |
| t.Errorf("%s: WaitForReady: %v (%v)", name, err, ph) |
| return nil |
| } |
| if !expectMessage(stderr, result) { |
| t.Errorf("%s: failed to read '%s' from child\n", name, result) |
| } |
| return ph |
| } |
| |
| func readyHelper(t *testing.T, name, test, result string) *ParentHandle { |
| cmd := helperCommand(test) |
| return readyHelperCmd(t, cmd, name, result) |
| } |
| |
| func testMany(t *testing.T, name, test, result string) []*ParentHandle { |
| nprocs := 10 |
| ph := make([]*ParentHandle, nprocs) |
| cmd := make([]*exec.Cmd, nprocs) |
| stderr := make([]io.ReadCloser, nprocs) |
| controlReaders := make([]io.ReadCloser, nprocs) |
| var done sync.WaitGroup |
| for i := 0; i < nprocs; i++ { |
| cmd[i] = helperCommand(test) |
| // The control pipe is used to signal the child when to wake up. |
| controlRead, controlWrite, err := os.Pipe() |
| if err != nil { |
| t.Errorf("Failed to create control pipe: %v", err) |
| return nil |
| } |
| controlReaders[i] = controlRead |
| cmd[i].ExtraFiles = append(cmd[i].ExtraFiles, controlRead) |
| stderr[i], _ = cmd[i].StderrPipe() |
| tk := timekeeper.NewManualTime() |
| ph[i] = NewParentHandle(cmd[i], "", TimeKeeperOpt{tk: tk}) |
| done.Add(1) |
| go func() { |
| // For simulated slow children, wait until the parent |
| // starts waiting, and then wake up the child. |
| if test == "testReadySlow" { |
| <-tk.Requests() |
| tk.AdvanceTime(3 * time.Second) |
| if _, err = controlWrite.Write([]byte("wake")); err != nil { |
| t.Errorf("Failed to write to control pipe: %v", err) |
| } |
| } |
| controlWrite.Close() |
| done.Done() |
| }() |
| if err := ph[i].Start(); err != nil { |
| t.Errorf("%s: Failed to start child %d: %s\n", name, i, err) |
| } |
| } |
| for i := 0; i < nprocs; i++ { |
| if err := ph[i].WaitForReady(5 * time.Second); err != nil { |
| t.Errorf("%s: Failed to wait for child %d: %s\n", name, i, err) |
| } |
| controlReaders[i].Close() |
| } |
| for i := 0; i < nprocs; i++ { |
| if !expectMessage(stderr[i], result) { |
| t.Errorf("%s: Failed to read message from child %d\n", name, i) |
| } |
| } |
| done.Wait() |
| return ph |
| } |
| |
| func TestToReadyMany(t *testing.T) { |
| clean(t, testMany(t, "TestToReadyMany", "testReady", ".")...) |
| } |
| |
| func TestToReadySlowMany(t *testing.T) { |
| clean(t, testMany(t, "TestToReadySlowMany", "testReadySlow", "..")...) |
| } |
| |
| func TestToReady(t *testing.T) { |
| ph := readyHelper(t, "TestToReady", "testReady", ".") |
| clean(t, ph) |
| } |
| |
| func TestNeverReady(t *testing.T) { |
| name := "testNeverReady" |
| result := "never ready" |
| cmd := helperCommand(name) |
| stderr, _ := cmd.StderrPipe() |
| ph := NewParentHandle(cmd, "") |
| err := waitForReady(t, cmd, name, 1, ph) |
| if err != ErrTimeout { |
| t.Errorf("Failed to get timeout: got %v\n", err) |
| } else { |
| // block waiting for error from child |
| if !expectMessage(stderr, result) { |
| t.Errorf("%s: failed to read '%s' from child\n", name, result) |
| } |
| } |
| clean(t, ph) |
| } |
| |
| func TestTooSlowToReady(t *testing.T) { |
| name := "testTooSlowToReady" |
| result := "write status_wr: broken pipe" |
| cmd := helperCommand(name) |
| // The control pipe is used to signal the child when to wake up. |
| controlRead, controlWrite, err := os.Pipe() |
| if err != nil { |
| t.Errorf("Failed to create control pipe: %v", err) |
| return |
| } |
| cmd.ExtraFiles = append(cmd.ExtraFiles, controlRead) |
| stderr, _ := cmd.StderrPipe() |
| tk := timekeeper.NewManualTime() |
| ph := NewParentHandle(cmd, "", TimeKeeperOpt{tk: tk}) |
| defer clean(t, ph) |
| defer controlWrite.Close() |
| defer controlRead.Close() |
| // Wait for the parent to start waiting, then simulate a timeout. |
| go func() { |
| toWait := <-tk.Requests() |
| tk.AdvanceTime(toWait) |
| }() |
| err = waitForReady(t, cmd, name, 1, ph) |
| if err != ErrTimeout { |
| t.Errorf("Failed to get timeout: got %v\n", err) |
| } else { |
| // After the parent timed out, wake up the child and let it |
| // proceed. |
| if _, err = controlWrite.Write([]byte("wake")); err != nil { |
| t.Errorf("Failed to write to control pipe: %v", err) |
| } else { |
| // block waiting for error from child |
| if !expectMessage(stderr, result) { |
| t.Errorf("%s: failed to read '%s' from child\n", name, result) |
| } |
| } |
| } |
| } |
| |
| func TestToReadySlow(t *testing.T) { |
| name := "TestToReadySlow" |
| cmd := helperCommand("testReadySlow") |
| // The control pipe is used to signal the child when to wake up. |
| controlRead, controlWrite, err := os.Pipe() |
| if err != nil { |
| t.Errorf("Failed to create control pipe: %v", err) |
| return |
| } |
| cmd.ExtraFiles = append(cmd.ExtraFiles, controlRead) |
| stderr, err := cmd.StderrPipe() |
| if err != nil { |
| t.Fatalf("%s: failed to get stderr pipe", name) |
| } |
| tk := timekeeper.NewManualTime() |
| ph := NewParentHandle(cmd, "", TimeKeeperOpt{tk: tk}) |
| defer clean(t, ph) |
| defer controlWrite.Close() |
| defer controlRead.Close() |
| // Wait for the parent to start waiting, simulate a short wait (but not |
| // enough to timeout the parent), then wake up the child. |
| go func() { |
| <-tk.Requests() |
| tk.AdvanceTime(2 * time.Second) |
| if _, err = controlWrite.Write([]byte("wake")); err != nil { |
| t.Errorf("Failed to write to control pipe: %v", err) |
| } |
| }() |
| if err := waitForReady(t, cmd, name, 4, ph); err != nil { |
| t.Errorf("%s: WaitForReady: %v (%v)", name, err, ph) |
| return |
| } |
| // After the child has replied, simulate a timeout on the server by |
| // advacing the time more; at this point, however, the timeout should no |
| // longer occur since the child already replied. |
| tk.AdvanceTime(2 * time.Second) |
| if result := ".."; !expectMessage(stderr, result) { |
| t.Errorf("%s: failed to read '%s' from child\n", name, result) |
| } |
| } |
| |
| func TestToCompletion(t *testing.T) { |
| ph := readyHelper(t, "TestToCompletion", "testSuccess", "...ok") |
| e := ph.Wait(time.Second) |
| if e != nil { |
| t.Errorf("Wait failed: err %s\n", e) |
| } |
| clean(t, ph) |
| } |
| |
| func TestToCompletionError(t *testing.T) { |
| ph := readyHelper(t, "TestToCompletionError", "testError", "...err") |
| e := ph.Wait(time.Second) |
| if e == nil { |
| t.Errorf("Wait failed: err %s\n", e) |
| } |
| clean(t, ph) |
| } |
| |
| func TestExtraFiles(t *testing.T) { |
| cmd := helperCommand("testExtraFiles") |
| rd, wr, err := os.Pipe() |
| if err != nil { |
| t.Fatalf("Failed to create pipe: %s\n", err) |
| } |
| cmd.ExtraFiles = append(cmd.ExtraFiles, rd) |
| msg := "hello there..." |
| fmt.Fprintf(wr, msg) |
| ph := readyHelperCmd(t, cmd, "TestExtraFiles", "child: "+msg) |
| if ph == nil { |
| t.Fatalf("Failed to get ParentHandle\n") |
| } |
| e := ph.Wait(1 * time.Second) |
| if e != nil { |
| t.Errorf("Wait failed: err %s\n", e) |
| } |
| rd.Close() |
| wr.Close() |
| clean(t, ph) |
| } |
| |
| // TestHelperProcess isn't a real test; it's used as a helper process |
| // for the other tests. |
| func TestHelperProcess(*testing.T) { |
| // Return immediately if this is not run as the child helper |
| // for the other tests. |
| if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { |
| return |
| } |
| defer os.Exit(0) |
| |
| // Write errors to stderr or using log. since the parent |
| // process is reading stderr. |
| args := os.Args |
| for len(args) > 0 { |
| if args[0] == "--" { |
| args = args[1:] |
| break |
| } |
| args = args[1:] |
| } |
| |
| if len(args) == 0 { |
| log.Fatal(os.Stderr, "No command\n") |
| } |
| |
| cmd, args := args[0], args[1:] |
| |
| switch cmd { |
| case "testNeverReady": |
| _, err := NewChildHandle() |
| if err != nil { |
| log.Fatal(os.Stderr, "%s\n", err) |
| } |
| fmt.Fprintf(os.Stderr, "never ready") |
| case "testTooSlowToReady": |
| ch, err := NewChildHandle() |
| if err != nil { |
| log.Fatal(os.Stderr, "%s\n", err) |
| } |
| // Wait for the parent to tell us when it's ok to proceed. |
| controlPipe := ch.NewExtraFile(0, "control_rd") |
| for { |
| buf := make([]byte, 100) |
| n, err := controlPipe.Read(buf) |
| if err != nil { |
| log.Fatal(os.Stderr, "%s", err) |
| } |
| if n > 0 { |
| break |
| } |
| } |
| // SetReady should return an error since the parent has |
| // timed out on us and we'd be writing to a closed pipe. |
| if err := ch.SetReady(); err != nil { |
| fmt.Fprintf(os.Stderr, "%s", err) |
| } else { |
| fmt.Fprintf(os.Stderr, "didn't get the expected error") |
| } |
| os.Exit(0) |
| case "testReady": |
| ch, err := NewChildHandle() |
| if err != nil { |
| log.Fatal(os.Stderr, "%s", err) |
| } |
| ch.SetReady() |
| fmt.Fprintf(os.Stderr, ".") |
| case "testReadySlow": |
| ch, err := NewChildHandle() |
| if err != nil { |
| log.Fatal(os.Stderr, "%s", err) |
| } |
| // Wait for the parent to tell us when it's ok to proceed. |
| controlPipe := ch.NewExtraFile(0, "control_rd") |
| for { |
| buf := make([]byte, 100) |
| n, err := controlPipe.Read(buf) |
| if err != nil { |
| log.Fatal(os.Stderr, "%s", err) |
| } |
| if n > 0 { |
| break |
| } |
| } |
| ch.SetReady() |
| fmt.Fprintf(os.Stderr, "..") |
| case "testSuccess", "testError": |
| ch, err := NewChildHandle() |
| if err != nil { |
| log.Fatal(os.Stderr, "%s\n", err) |
| } |
| ch.SetReady() |
| rc := make(chan int) |
| go func() { |
| if cmd == "testError" { |
| fmt.Fprintf(os.Stderr, "...err") |
| rc <- 1 |
| } else { |
| fmt.Fprintf(os.Stderr, "...ok") |
| rc <- 0 |
| } |
| }() |
| r := <-rc |
| os.Exit(r) |
| case "testAuth": |
| ch, err := NewChildHandle() |
| if err != nil { |
| log.Fatalf("%s", err) |
| } else { |
| fmt.Fprintf(os.Stderr, "%s", ch.Secret) |
| } |
| case "testExtraFiles": |
| ch, err := NewChildHandle() |
| if err != nil { |
| log.Fatal("error.... %s\n", err) |
| } |
| err = ch.SetReady() |
| rd := ch.NewExtraFile(0, "read") |
| buf := make([]byte, 1024) |
| if n, err := rd.Read(buf); err != nil { |
| fmt.Fprintf(os.Stderr, "child: error %s\n", err) |
| } else { |
| fmt.Fprintf(os.Stderr, "child: %s", string(buf[:n])) |
| } |
| os.Exit(0) |
| } |
| } |