package spawn import ( "fmt" "io/ioutil" "os" "os/exec" "strings" "testing" "time" "github.com/hashicorp/nomad/helper/testtask" ) func TestMain(m *testing.M) { if !testtask.Run() { os.Exit(m.Run()) } } func TestSpawn_NoCmd(t *testing.T) { t.Parallel() tempFile := tempFileName(t) defer os.Remove(tempFile) spawn := NewSpawner(tempFile) if err := spawn.Spawn(nil); err == nil { t.Fatalf("Spawn() with no user command should fail") } } func TestSpawn_InvalidCmd(t *testing.T) { t.Parallel() tempFile := tempFileName(t) defer os.Remove(tempFile) spawn := NewSpawner(tempFile) spawn.SetCommand(exec.Command("foo")) // non-existent command if err := spawn.Spawn(nil); err == nil { t.Fatalf("Spawn() with an invalid command should fail") } } func TestSpawn_SetsLogs(t *testing.T) { t.Parallel() tempFile := tempFileName(t) defer os.Remove(tempFile) spawn := NewSpawner(tempFile) exp := "foo" spawn.SetCommand(testCommand("echo", exp)) // Create file for stdout. stdout := tempFileName(t) defer os.Remove(stdout) spawn.SetLogs(&Logs{Stdout: stdout}) if err := spawn.Spawn(nil); err != nil { t.Fatalf("Spawn() failed: %v", err) } if res := spawn.Wait(); res.ExitCode != 0 && res.Err != nil { t.Fatalf("Wait() returned %v, %v; want 0, nil", res.ExitCode, res.Err) } stdout2, err := os.Open(stdout) if err != nil { t.Fatalf("Open() failed: %v", err) } data, err := ioutil.ReadAll(stdout2) if err != nil { t.Fatalf("ReadAll() failed: %v", err) } act := strings.TrimSpace(string(data)) if act != exp { t.Fatalf("Unexpected data written to stdout; got %v; want %v", act, exp) } } func TestSpawn_Callback(t *testing.T) { t.Parallel() tempFile := tempFileName(t) defer os.Remove(tempFile) spawn := NewSpawner(tempFile) spawn.SetCommand(testCommand("sleep", "1s")) called := false cbErr := fmt.Errorf("ERROR CB") cb := func(_ int) error { called = true return cbErr } if err := spawn.Spawn(cb); err == nil { t.Fatalf("Spawn(%#v) should have errored; want %v", cb, cbErr) } if !called { t.Fatalf("Spawn(%#v) didn't call callback", cb) } } func TestSpawn_ParentWaitExited(t *testing.T) { t.Parallel() tempFile := tempFileName(t) defer os.Remove(tempFile) spawn := NewSpawner(tempFile) spawn.SetCommand(testCommand("echo", "foo")) if err := spawn.Spawn(nil); err != nil { t.Fatalf("Spawn() failed %v", err) } time.Sleep(1 * time.Second) if res := spawn.Wait(); res.ExitCode != 0 && res.Err != nil { t.Fatalf("Wait() returned %v, %v; want 0, nil", res.ExitCode, res.Err) } } func TestSpawn_ParentWait(t *testing.T) { t.Parallel() tempFile := tempFileName(t) defer os.Remove(tempFile) spawn := NewSpawner(tempFile) spawn.SetCommand(testCommand("sleep", "2s")) if err := spawn.Spawn(nil); err != nil { t.Fatalf("Spawn() failed %v", err) } if res := spawn.Wait(); res.ExitCode != 0 && res.Err != nil { t.Fatalf("Wait() returned %v, %v; want 0, nil", res.ExitCode, res.Err) } } func TestSpawn_NonParentWaitExited(t *testing.T) { t.Parallel() tempFile := tempFileName(t) defer os.Remove(tempFile) spawn := NewSpawner(tempFile) spawn.SetCommand(testCommand("echo", "foo")) if err := spawn.Spawn(nil); err != nil { t.Fatalf("Spawn() failed %v", err) } time.Sleep(1 * time.Second) // Force the wait to assume non-parent. spawn.SpawnPpid = 0 if res := spawn.Wait(); res.ExitCode != 0 && res.Err != nil { t.Fatalf("Wait() returned %v, %v; want 0, nil", res.ExitCode, res.Err) } } func TestSpawn_NonParentWait(t *testing.T) { t.Parallel() tempFile := tempFileName(t) defer os.Remove(tempFile) spawn := NewSpawner(tempFile) spawn.SetCommand(testCommand("sleep", "2s")) if err := spawn.Spawn(nil); err != nil { t.Fatalf("Spawn() failed %v", err) } // Need to wait on the spawner, otherwise it becomes a zombie and the test // only finishes after the init process cleans it. This speeds that up. go func() { time.Sleep(3 * time.Second) if _, err := spawn.spawn.Wait(); err != nil { t.FailNow() } }() // Force the wait to assume non-parent. spawn.SpawnPpid = 0 if res := spawn.Wait(); res.ExitCode != 0 && res.Err != nil { t.Fatalf("Wait() returned %v, %v; want 0, nil", res.ExitCode, res.Err) } } func TestSpawn_DeadSpawnDaemon_Parent(t *testing.T) { t.Parallel() tempFile := tempFileName(t) defer os.Remove(tempFile) var spawnPid int cb := func(pid int) error { spawnPid = pid return nil } spawn := NewSpawner(tempFile) spawn.SetCommand(testCommand("sleep", "5s")) if err := spawn.Spawn(cb); err != nil { t.Fatalf("Spawn() errored: %v", err) } proc, err := os.FindProcess(spawnPid) if err != nil { t.FailNow() } if err := proc.Kill(); err != nil { t.FailNow() } if _, err := proc.Wait(); err != nil { t.FailNow() } if res := spawn.Wait(); res.Err == nil { t.Fatalf("Wait() should have failed: %v", res.Err) } } func TestSpawn_DeadSpawnDaemon_NonParent(t *testing.T) { t.Parallel() tempFile := tempFileName(t) defer os.Remove(tempFile) var spawnPid int cb := func(pid int) error { spawnPid = pid return nil } spawn := NewSpawner(tempFile) spawn.SetCommand(testCommand("sleep", "2s")) if err := spawn.Spawn(cb); err != nil { t.Fatalf("Spawn() errored: %v", err) } proc, err := os.FindProcess(spawnPid) if err != nil { t.FailNow() } if err := proc.Kill(); err != nil { t.FailNow() } if _, err := proc.Wait(); err != nil { t.FailNow() } // Force the wait to assume non-parent. spawn.SpawnPpid = 0 if res := spawn.Wait(); res.Err == nil { t.Fatalf("Wait() should have failed: %v", res.Err) } } func TestSpawn_Valid_TaskRunning(t *testing.T) { t.Parallel() tempFile := tempFileName(t) defer os.Remove(tempFile) spawn := NewSpawner(tempFile) spawn.SetCommand(testCommand("sleep", "2s")) if err := spawn.Spawn(nil); err != nil { t.Fatalf("Spawn() failed %v", err) } if err := spawn.Valid(); err != nil { t.Fatalf("Valid() failed: %v", err) } if res := spawn.Wait(); res.Err != nil { t.Fatalf("Wait() failed: %v", res.Err) } } func TestSpawn_Valid_TaskExit_ExitCode(t *testing.T) { t.Parallel() tempFile := tempFileName(t) defer os.Remove(tempFile) spawn := NewSpawner(tempFile) spawn.SetCommand(testCommand("echo", "foo")) if err := spawn.Spawn(nil); err != nil { t.Fatalf("Spawn() failed %v", err) } if res := spawn.Wait(); res.Err != nil { t.Fatalf("Wait() failed: %v", res.Err) } if err := spawn.Valid(); err != nil { t.Fatalf("Valid() failed: %v", err) } } func TestSpawn_Valid_TaskExit_NoExitCode(t *testing.T) { t.Parallel() tempFile := tempFileName(t) defer os.Remove(tempFile) spawn := NewSpawner(tempFile) spawn.SetCommand(testCommand("echo", "foo")) if err := spawn.Spawn(nil); err != nil { t.Fatalf("Spawn() failed %v", err) } if res := spawn.Wait(); res.Err != nil { t.Fatalf("Wait() failed: %v", res.Err) } // Delete the file so that it can't find the exit code. os.Remove(tempFile) if err := spawn.Valid(); err == nil { t.Fatalf("Valid() should have failed") } } func tempFileName(t *testing.T) string { f, err := ioutil.TempFile("", "") if err != nil { t.Fatalf("TempFile() failed") } defer f.Close() return f.Name() } func testCommand(args ...string) *exec.Cmd { cmd := exec.Command(testtask.Path(), args...) testtask.SetCmdEnv(cmd) return cmd }