diff options
Diffstat (limited to 'runsc/test')
-rw-r--r-- | runsc/test/integration/exec_test.go | 55 | ||||
-rw-r--r-- | runsc/test/testutil/BUILD | 1 | ||||
-rw-r--r-- | runsc/test/testutil/docker.go | 21 | ||||
-rw-r--r-- | runsc/test/testutil/testutil.go | 36 |
4 files changed, 113 insertions, 0 deletions
diff --git a/runsc/test/integration/exec_test.go b/runsc/test/integration/exec_test.go index 910c36597..ddd088223 100644 --- a/runsc/test/integration/exec_test.go +++ b/runsc/test/integration/exec_test.go @@ -27,6 +27,7 @@ package integration import ( + "syscall" "testing" "time" @@ -60,3 +61,57 @@ func TestExecCapabilities(t *testing.T) { t.Errorf("wrong capabilities, got: %q, want: %q", got, want) } } + +func TestExecJobControl(t *testing.T) { + if err := testutil.Pull("alpine"); err != nil { + t.Fatalf("docker pull failed: %v", err) + } + d := testutil.MakeDocker("exec-test") + + // Start the container. + if _, err := d.Run("alpine", "sleep", "1000"); err != nil { + t.Fatalf("docker run failed: %v", err) + } + defer d.CleanUp() + + // Exec 'sh' with an attached pty. + cmd, ptmx, err := d.ExecWithTerminal("sh") + if err != nil { + t.Fatalf("docker exec failed: %v", err) + } + defer ptmx.Close() + + // Call "sleep 100" in the shell. + if _, err := ptmx.Write([]byte("sleep 100\n")); err != nil { + t.Fatalf("error writing to pty: %v", err) + } + + // Give shell a few seconds to start executing the sleep. + time.Sleep(2 * time.Second) + + // Send a ^C to the pty, which should kill sleep, but not the shell. + // \x03 is ASCII "end of text", which is the same as ^C. + if _, err := ptmx.Write([]byte{'\x03'}); err != nil { + t.Fatalf("error writing to pty: %v", err) + } + + // The shell should still be alive at this point. Sleep should have + // exited with code 2+128=130. We'll exit with 10 plus that number, so + // that we can be sure that the shell did not get signalled. + if _, err := ptmx.Write([]byte("exit $(expr $? + 10)\n")); err != nil { + t.Fatalf("error writing to pty: %v", err) + } + + // Exec process should exit with code 10+130=140. + ps, err := cmd.Process.Wait() + if err != nil { + t.Fatalf("error waiting for exec process: %v", err) + } + ws := ps.Sys().(syscall.WaitStatus) + if !ws.Exited() { + t.Errorf("ws.Exited got false, want true") + } + if got, want := ws.ExitStatus(), 140; got != want { + t.Errorf("ws.ExitedStatus got %d, want %d", got, want) + } +} diff --git a/runsc/test/testutil/BUILD b/runsc/test/testutil/BUILD index ca91e07ff..da2535bfa 100644 --- a/runsc/test/testutil/BUILD +++ b/runsc/test/testutil/BUILD @@ -17,6 +17,7 @@ go_library( "//runsc/boot", "//runsc/specutils", "@com_github_cenkalti_backoff//:go_default_library", + "@com_github_kr_pty//:go_default_library", "@com_github_opencontainers_runtime-spec//specs-go:go_default_library", "@com_github_syndtr_gocapability//capability:go_default_library", ], diff --git a/runsc/test/testutil/docker.go b/runsc/test/testutil/docker.go index 7f5909987..55ca353b8 100644 --- a/runsc/test/testutil/docker.go +++ b/runsc/test/testutil/docker.go @@ -26,6 +26,8 @@ import ( "strconv" "strings" "time" + + "github.com/kr/pty" ) func init() { @@ -131,6 +133,17 @@ func do(args ...string) (string, error) { return string(out), nil } +// doWithPty executes docker command with stdio attached to a pty. +func doWithPty(args ...string) (*exec.Cmd, *os.File, error) { + fmt.Printf("Running with pty: docker %s\n", args) + cmd := exec.Command("docker", args...) + ptmx, err := pty.Start(cmd) + if err != nil { + return nil, nil, fmt.Errorf("error executing docker %s with a pty: %v", args, err) + } + return cmd, ptmx, nil +} + // Pull pulls a docker image. This is used in tests to isolate the // time to pull the image off the network from the time to actually // start the container, to avoid timeouts over slow networks. @@ -197,6 +210,14 @@ func (d *Docker) Exec(args ...string) (string, error) { return do(a...) } +// ExecWithTerminal calls 'docker exec -it' with the arguments provided and +// attaches a pty to stdio. +func (d *Docker) ExecWithTerminal(args ...string) (*exec.Cmd, *os.File, error) { + a := []string{"exec", "-it", d.Name} + a = append(a, args...) + return doWithPty(a...) +} + // Pause calls 'docker pause'. func (d *Docker) Pause() error { if _, err := do("pause", d.Name); err != nil { diff --git a/runsc/test/testutil/testutil.go b/runsc/test/testutil/testutil.go index 07d66e469..cdc7f78c3 100644 --- a/runsc/test/testutil/testutil.go +++ b/runsc/test/testutil/testutil.go @@ -16,6 +16,7 @@ package testutil import ( + "bufio" "context" "encoding/json" "fmt" @@ -27,6 +28,8 @@ import ( "os/signal" "path/filepath" "runtime" + "strings" + "sync/atomic" "syscall" "time" @@ -315,3 +318,36 @@ func RetryEintr(f func() (uintptr, uintptr, error)) (uintptr, uintptr, error) { } } } + +// WaitUntilRead reads from the given reader until the wanted string is found +// or until timeout. +func WaitUntilRead(r io.Reader, want string, split bufio.SplitFunc, timeout time.Duration) error { + sc := bufio.NewScanner(r) + if split != nil { + sc.Split(split) + } + // done must be accessed atomically. A value greater than 0 indicates + // that the read loop can exit. + var done uint32 + doneCh := make(chan struct{}) + go func() { + for sc.Scan() { + t := sc.Text() + if strings.Contains(t, want) { + atomic.StoreUint32(&done, 1) + close(doneCh) + break + } + if atomic.LoadUint32(&done) > 0 { + break + } + } + }() + select { + case <-time.After(timeout): + atomic.StoreUint32(&done, 1) + return fmt.Errorf("timeout waiting to read %q", want) + case <-doneCh: + return nil + } +} |