blob: b0f2452960fda73ffee8bc5b71a2d16e96543ed7 [file] [log] [blame]
// Copyright 2018 The Chromium OS 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 testexec
import (
"context"
"os/exec"
"strings"
gotesting "testing"
"time"
"github.com/shirou/gopsutil/process"
"chromiumos/tast/errors"
tastexec "chromiumos/tast/exec"
"chromiumos/tast/testing"
)
func TestKillAll(t *gotesting.T) {
ctx, cancel := context.WithCancel(context.Background())
defer func() {
if cancel != nil {
cancel()
}
}()
cmd := CommandContext(ctx, "sh", "-c", "sleep 60; true")
if err := cmd.Start(); err != nil {
t.Fatal("Failed to start a shell: ", err)
}
var grandchild *process.Process
for grandchild == nil {
ps, err := process.Processes()
if err != nil {
t.Fatal("Failed to enumerate processes: ", err)
}
for _, p := range ps {
ppid, err := p.Ppid()
if err == nil && int(ppid) == cmd.Process.Pid {
grandchild = p
break
}
}
}
cancel()
cancel = nil
cmd.Wait()
// cancel() above should trigger sending SIGKILL to both the child and the grandchild processes.
// However, there's no guarantee of the order, so there is small chance that the grandchild
// is still alive at this moment.
// Thus, poll until the grandchild is gone for a short period. In most cases it should
// be done immediately, so wait for 3 secs (arbitrary time).
if err := testing.Poll(context.Background(), func(ctx context.Context) error {
if status, err := grandchild.Status(); err == nil {
return errors.Errorf("grandchild process still running: pid=%d, status=%s", grandchild.Pid, status)
}
// The grandchild process is terminated and collected.
return nil
}, &testing.PollOptions{Timeout: 3 * time.Second}); err != nil {
t.Error("Grandchild process was not killed on cancel: ", err)
}
}
func TestAutoCollect(t *gotesting.T) {
cmd := CommandContext(context.Background(), "sh", "-c", "echo foo; echo bar >&2")
if err := cmd.Run(); err != nil {
t.Fatal(err)
}
if !strings.Contains(cmd.log.String(), "foo") {
t.Errorf("Run: log %q does not contain %q", cmd.log.String(), "foo")
}
if !strings.Contains(cmd.log.String(), "bar") {
t.Errorf("Run: log %q does not contain %q", cmd.log.String(), "bar")
}
cmd = CommandContext(context.Background(), "sh", "-c", "echo foo; echo bar >&2")
if _, err := cmd.Output(); err != nil {
t.Fatal(err)
}
if strings.Contains(cmd.log.String(), "foo") {
t.Errorf("Output: log %q contains %q", cmd.log.String(), "foo")
}
if !strings.Contains(cmd.log.String(), "bar") {
t.Errorf("Output: log %q does not contain %q", cmd.log.String(), "bar")
}
cmd = CommandContext(context.Background(), "sh", "-c", "echo foo; echo bar >&2")
if _, err := cmd.CombinedOutput(); err != nil {
t.Fatal(err)
}
if strings.Contains(cmd.log.String(), "foo") {
t.Errorf("CombinedOutput: log %q contains %q", cmd.log.String(), "foo")
}
if strings.Contains(cmd.log.String(), "bar") {
t.Errorf("CombinedOutput: log %q contains %q", cmd.log.String(), "bar")
}
cmd = CommandContext(context.Background(), "sh", "-c", "echo foo; echo bar >&2")
if _, _, err := cmd.SeparatedOutput(); err != nil {
t.Fatal(err)
}
if strings.Contains(cmd.log.String(), "foo") {
t.Errorf("SeparatedOutput: log %q contains %q", cmd.log.String(), "foo")
}
if strings.Contains(cmd.log.String(), "bar") {
t.Errorf("SeparatedOutput: log %q contains %q", cmd.log.String(), "bar")
}
}
func TestOutput(t *gotesting.T) {
b, err := CommandContext(context.Background(), "sh", "-c", "echo foo; echo bar >&2").Output()
if err != nil {
t.Error(err)
} else {
s := string(b)
if !strings.Contains(s, "foo") {
t.Errorf("Output: %q does not contain %q", s, "foo")
}
if strings.Contains(s, "bar") {
t.Errorf("Output: %q contains %q", s, "bar")
}
}
b, err = CommandContext(context.Background(), "sh", "-c", "echo foo; echo bar >&2").CombinedOutput()
if err != nil {
t.Error(err)
} else {
s := string(b)
if !strings.Contains(s, "foo") {
t.Errorf("CombinedOutput: %q does not contain %q", s, "foo")
}
if !strings.Contains(s, "bar") {
t.Errorf("CombinedOutput: %q does not contain %q", s, "bar")
}
}
bo, be, err := CommandContext(context.Background(), "sh", "-c", "echo foo; echo bar >&2").SeparatedOutput()
if err != nil {
t.Error(err)
} else {
so := string(bo)
if !strings.Contains(so, "foo") {
t.Errorf("SeparatedOutput: stdout %q does not contain %q", so, "foo")
}
if strings.Contains(so, "bar") {
t.Errorf("SeparatedOutput: stdout %q contains %q", so, "bar")
}
se := string(be)
if strings.Contains(se, "foo") {
t.Errorf("SeparatedOutput: stderr %q contains %q", se, "foo")
}
if !strings.Contains(se, "bar") {
t.Errorf("SeparatedOutput: stderr %q does not contain %q", se, "bar")
}
}
}
func TestGetWaitStatus(t *gotesting.T) {
err28 := exec.Command("sh", "-c", "exit 28").Run()
for _, c := range []struct {
err error
code int
ok bool
}{
{nil, 0, true},
{err28, 28, true},
{errors.New("foo"), 0, false},
} {
status, ok := GetWaitStatus(c.err)
code := status.ExitStatus()
if ok != c.ok || status.ExitStatus() != c.code {
t.Errorf("GetWaitStatus(%#v) = (%v, %v); want (%v, %v)", c.err, code, ok, c.code, c.ok)
}
}
}
func TestExitCode(t *gotesting.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
err28 := exec.Command("sh", "-c", "exit 28").Run()
killed := CommandContext(ctx, "sleep", "3s")
if err := killed.Start(); err != nil {
t.Fatal("Failed to start a process: ", err)
}
if err := killed.Kill(); err != nil {
t.Fatal("Failed to kill a process: ", err)
}
errKilled := killed.Wait()
for _, c := range []struct {
err error
code int
ok bool
}{
{nil, 0, true},
{err28, 28, true},
{errKilled, 128 + 9 /* SIGKILL */, true},
{errors.New("foo"), 0, false},
} {
code, ok := ExitCode(c.err)
if ok != c.ok || code != c.code {
t.Errorf("ExitCode(%#v) = (%v, %v); want (%v, %v)", c.err, code, ok, c.code, c.ok)
}
}
}
type sshOrLocal interface {
Run(opts ...tastexec.RunOption) error
Output(opts ...tastexec.RunOption) ([]byte, error)
CombinedOutput(opts ...tastexec.RunOption) ([]byte, error)
Start() error
Wait(opts ...tastexec.RunOption) error
DumpLog(ctx context.Context) error
}
// TestCast verifies the return value of CommandContext can be assigned to an interface that also works for ssh Cmd.
func TestCast(t *gotesting.T) {
var cmd sshOrLocal
cmd = CommandContext(context.Background(), "true")
cmd.Run()
}