blob: 6231ac6cf200701322b85a02ca6a41b14a02ba94 [file] [log] [blame]
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)
}
}