engine: resources: Improve exec resource

The exec resource was an early addition to the project, and it was due
for some fixes and integration into our automated tests. This patch
fixes a number of issues, and makes it ready for more general use.
This commit is contained in:
James Shubin
2019-03-02 21:14:01 -05:00
parent 829741e2ac
commit 6b803656b2
5 changed files with 470 additions and 162 deletions

View File

@@ -20,20 +20,34 @@
package resources
import (
"context"
"fmt"
"os/exec"
"syscall"
"testing"
"time"
"github.com/purpleidea/mgmt/engine"
)
func fakeInit(t *testing.T) *engine.Init {
func fakeExecInit(t *testing.T) (*engine.Init, *ExecSends) {
debug := testing.Verbose() // set via the -test.v flag to `go test`
logf := func(format string, v ...interface{}) {
t.Logf("test: "+format, v...)
}
execSends := &ExecSends{}
return &engine.Init{
Send: func(st interface{}) error {
x, ok := st.(*ExecSends)
if !ok {
return fmt.Errorf("unable to send")
}
*execSends = *x // set
return nil
},
Debug: debug,
Logf: logf,
}
}, execSends
}
func TestExecSendRecv1(t *testing.T) {
@@ -50,7 +64,8 @@ func TestExecSendRecv1(t *testing.T) {
t.Errorf("close failed with: %v", err)
}
}()
if err := r1.Init(fakeInit(t)); err != nil {
init, execSends := fakeExecInit(t)
if err := r1.Init(init); err != nil {
t.Errorf("init failed with: %v", err)
}
// run artificially without the entire engine
@@ -58,23 +73,23 @@ func TestExecSendRecv1(t *testing.T) {
t.Errorf("checkapply failed with: %v", err)
}
t.Logf("output is: %v", r1.Output)
if r1.Output != nil {
t.Logf("output is: %v", *r1.Output)
t.Logf("output is: %v", execSends.Output)
if execSends.Output != nil {
t.Logf("output is: %v", *execSends.Output)
}
t.Logf("stdout is: %v", r1.Stdout)
if r1.Stdout != nil {
t.Logf("stdout is: %v", *r1.Stdout)
t.Logf("stdout is: %v", execSends.Stdout)
if execSends.Stdout != nil {
t.Logf("stdout is: %v", *execSends.Stdout)
}
t.Logf("stderr is: %v", r1.Stderr)
if r1.Stderr != nil {
t.Logf("stderr is: %v", *r1.Stderr)
t.Logf("stderr is: %v", execSends.Stderr)
if execSends.Stderr != nil {
t.Logf("stderr is: %v", *execSends.Stderr)
}
if r1.Stdout == nil {
if execSends.Stdout == nil {
t.Errorf("stdout is nil")
} else {
if out := *r1.Stdout; out != "hello world\n" {
if out := *execSends.Stdout; out != "hello world\n" {
t.Errorf("got wrong stdout(%d): %s", len(out), out)
}
}
@@ -94,7 +109,8 @@ func TestExecSendRecv2(t *testing.T) {
t.Errorf("close failed with: %v", err)
}
}()
if err := r1.Init(fakeInit(t)); err != nil {
init, execSends := fakeExecInit(t)
if err := r1.Init(init); err != nil {
t.Errorf("init failed with: %v", err)
}
// run artificially without the entire engine
@@ -102,23 +118,23 @@ func TestExecSendRecv2(t *testing.T) {
t.Errorf("checkapply failed with: %v", err)
}
t.Logf("output is: %v", r1.Output)
if r1.Output != nil {
t.Logf("output is: %v", *r1.Output)
t.Logf("output is: %v", execSends.Output)
if execSends.Output != nil {
t.Logf("output is: %v", *execSends.Output)
}
t.Logf("stdout is: %v", r1.Stdout)
if r1.Stdout != nil {
t.Logf("stdout is: %v", *r1.Stdout)
t.Logf("stdout is: %v", execSends.Stdout)
if execSends.Stdout != nil {
t.Logf("stdout is: %v", *execSends.Stdout)
}
t.Logf("stderr is: %v", r1.Stderr)
if r1.Stderr != nil {
t.Logf("stderr is: %v", *r1.Stderr)
t.Logf("stderr is: %v", execSends.Stderr)
if execSends.Stderr != nil {
t.Logf("stderr is: %v", *execSends.Stderr)
}
if r1.Stderr == nil {
if execSends.Stderr == nil {
t.Errorf("stderr is nil")
} else {
if out := *r1.Stderr; out != "hello world\n" {
if out := *execSends.Stderr; out != "hello world\n" {
t.Errorf("got wrong stderr(%d): %s", len(out), out)
}
}
@@ -138,7 +154,8 @@ func TestExecSendRecv3(t *testing.T) {
t.Errorf("close failed with: %v", err)
}
}()
if err := r1.Init(fakeInit(t)); err != nil {
init, execSends := fakeExecInit(t)
if err := r1.Init(init); err != nil {
t.Errorf("init failed with: %v", err)
}
// run artificially without the entire engine
@@ -146,42 +163,97 @@ func TestExecSendRecv3(t *testing.T) {
t.Errorf("checkapply failed with: %v", err)
}
t.Logf("output is: %v", r1.Output)
if r1.Output != nil {
t.Logf("output is: %v", *r1.Output)
t.Logf("output is: %v", execSends.Output)
if execSends.Output != nil {
t.Logf("output is: %v", *execSends.Output)
}
t.Logf("stdout is: %v", r1.Stdout)
if r1.Stdout != nil {
t.Logf("stdout is: %v", *r1.Stdout)
t.Logf("stdout is: %v", execSends.Stdout)
if execSends.Stdout != nil {
t.Logf("stdout is: %v", *execSends.Stdout)
}
t.Logf("stderr is: %v", r1.Stderr)
if r1.Stderr != nil {
t.Logf("stderr is: %v", *r1.Stderr)
t.Logf("stderr is: %v", execSends.Stderr)
if execSends.Stderr != nil {
t.Logf("stderr is: %v", *execSends.Stderr)
}
if r1.Output == nil {
if execSends.Output == nil {
t.Errorf("output is nil")
} else {
// it looks like bash or golang race to the write, so whichever
// order they come out in is ok, as long as they come out whole
if out := *r1.Output; out != "hello world\ngoodbye world\n" && out != "goodbye world\nhello world\n" {
if out := *execSends.Output; out != "hello world\ngoodbye world\n" && out != "goodbye world\nhello world\n" {
t.Errorf("got wrong output(%d): %s", len(out), out)
}
}
if r1.Stdout == nil {
if execSends.Stdout == nil {
t.Errorf("stdout is nil")
} else {
if out := *r1.Stdout; out != "hello world\n" {
if out := *execSends.Stdout; out != "hello world\n" {
t.Errorf("got wrong stdout(%d): %s", len(out), out)
}
}
if r1.Stderr == nil {
if execSends.Stderr == nil {
t.Errorf("stderr is nil")
} else {
if out := *r1.Stderr; out != "goodbye world\n" {
if out := *execSends.Stderr; out != "goodbye world\n" {
t.Errorf("got wrong stderr(%d): %s", len(out), out)
}
}
}
func TestExecTimeoutBehaviour(t *testing.T) {
// cmd.Process.Kill() is called on timeout
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
cmdName := "/bin/sleep" // it's /usr/bin/sleep on modern distros
cmdArgs := []string{"300"} // 5 min in seconds
cmd := exec.CommandContext(ctx, cmdName, cmdArgs...)
// ignore signals sent to parent process (we're in our own group)
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
Pgid: 0,
}
if err := cmd.Start(); err != nil {
t.Errorf("error starting cmd: %+v", err)
return
}
err := cmd.Wait() // we can unblock this with the timeout
if err == nil {
t.Errorf("expected error, got nil")
return
}
exitErr, ok := err.(*exec.ExitError) // embeds an os.ProcessState
if err != nil && ok {
pStateSys := exitErr.Sys() // (*os.ProcessState) Sys
wStatus, ok := pStateSys.(syscall.WaitStatus)
if !ok {
t.Errorf("error running cmd")
return
}
if !wStatus.Signaled() {
t.Errorf("did not get signal, exit status: %d", wStatus.ExitStatus())
return
}
// we get this on timeout, because ctx calls cmd.Process.Kill()
if sig := wStatus.Signal(); sig != syscall.SIGKILL {
t.Errorf("got wrong signal: %+v, exit status: %d", sig, wStatus.ExitStatus())
return
}
t.Logf("exit status: %d", wStatus.ExitStatus())
return
} else if err != nil {
t.Errorf("general cmd error")
return
}
// no error
}