From 20a1829dadaf48d31f65063a9219ed6fd6ed437b Mon Sep 17 00:00:00 2001 From: Tim Hockin Date: Sun, 21 Sep 2014 21:33:02 -0700 Subject: [PATCH] Add a util/exec interface for testing execs. --- pkg/util/exec/doc.go | 18 ++++++++ pkg/util/exec/exec.go | 95 ++++++++++++++++++++++++++++++++++++++ pkg/util/exec/exec_test.go | 83 +++++++++++++++++++++++++++++++++ 3 files changed, 196 insertions(+) create mode 100644 pkg/util/exec/doc.go create mode 100644 pkg/util/exec/exec.go create mode 100644 pkg/util/exec/exec_test.go diff --git a/pkg/util/exec/doc.go b/pkg/util/exec/doc.go new file mode 100644 index 0000000000..a489bcfdfe --- /dev/null +++ b/pkg/util/exec/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package exec provides an injectable interface and implementations for running commands. +package exec diff --git a/pkg/util/exec/exec.go b/pkg/util/exec/exec.go new file mode 100644 index 0000000000..8052c3defe --- /dev/null +++ b/pkg/util/exec/exec.go @@ -0,0 +1,95 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package exec + +import ( + osexec "os/exec" + "syscall" +) + +// Interface is an interface that presents a subset of the os/exec API. Use this +// when you want to inject fakeable/mockable exec behavior. +type Interface interface { + // Command returns a Cmd instance which can be used to run a single command. + // This follows the pattern of package os/exec. + Command(cmd string, args ...string) Cmd +} + +// Cmd is an interface that presents an API that is very similar to Cmd from os/exec. +// As more functionality is needed, this can grow. Since Cmd is a struct, we will have +// to replace fields with get/set method pairs. +type Cmd interface { + // CombinedOutput runs the command and returns its combined standard output + // and standard error. This follows the pattern of package os/exec. + CombinedOutput() ([]byte, error) +} + +// ExitError is an interface that presents an API similar to os.ProcessState, which is +// what ExitError from os/exec is. This is designed to make testing a bit easier and +// probably loses some of the cross-platform properties of the underlying library. +type ExitError interface { + String() string + Error() string + Exited() bool + ExitStatus() int +} + +// Implements Interface in terms of really exec()ing. +type executor struct{} + +// New returns a new Interface which will os/exec to run commands. +func New() Interface { + return &executor{} +} + +// Command is part of the Interface interface. +func (executor *executor) Command(cmd string, args ...string) Cmd { + return (*cmdWrapper)(osexec.Command(cmd, args...)) +} + +// Wraps exec.Cmd so we can capture errors. +type cmdWrapper osexec.Cmd + +// CombinedOutput is part of the Cmd interface. +func (cmd *cmdWrapper) CombinedOutput() ([]byte, error) { + out, err := (*osexec.Cmd)(cmd).CombinedOutput() + if err != nil { + ee, ok := err.(*osexec.ExitError) + if !ok { + return out, err + } + // Force a compile fail if exitErrorWrapper can't convert to ExitError. + var x ExitError = &exitErrorWrapper{ee} + return out, x + } + return out, nil +} + +// exitErrorWrapper is an implementation of ExitError in terms of os/exec ExitError. +// Note: standard exec.ExitError is type *os.ProcessState, which already implements Exited(). +type exitErrorWrapper struct { + *osexec.ExitError +} + +// ExitStatus is part of the ExitError interface. +func (eew exitErrorWrapper) ExitStatus() int { + ws, ok := eew.Sys().(syscall.WaitStatus) + if !ok { + panic("can't call ExitStatus() on a non-WaitStatus exitErrorWrapper") + } + return ws.ExitStatus() +} diff --git a/pkg/util/exec/exec_test.go b/pkg/util/exec/exec_test.go new file mode 100644 index 0000000000..a42b786710 --- /dev/null +++ b/pkg/util/exec/exec_test.go @@ -0,0 +1,83 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package exec + +import ( + "testing" +) + +func TestExecutorNoArgs(t *testing.T) { + ex := New() + + cmd := ex.Command("/bin/true") + out, err := cmd.CombinedOutput() + if err != nil { + t.Errorf("expected success, got %+v", err) + } + if len(out) != 0 { + t.Errorf("expected no output, got %q", string(out)) + } + + cmd = ex.Command("/bin/false") + out, err = cmd.CombinedOutput() + if err == nil { + t.Errorf("expected failure, got nil error") + } + if len(out) != 0 { + t.Errorf("expected no output, got %q", string(out)) + } + ee, ok := err.(ExitError) + if !ok { + t.Errorf("expected an ExitError, got %+v", err) + } + if ee.Exited() { + if code := ee.ExitStatus(); code != 1 { + t.Errorf("expected exit status 1, got %d", code) + } + } + + cmd = ex.Command("/does/not/exist") + out, err = cmd.CombinedOutput() + if err == nil { + t.Errorf("expected failure, got nil error") + } + if ee, ok := err.(ExitError); ok { + t.Errorf("expected non-ExitError, got %+v", ee) + } +} + +func TestExecutorWithArgs(t *testing.T) { + ex := New() + + cmd := ex.Command("/bin/echo", "stdout") + out, err := cmd.CombinedOutput() + if err != nil { + t.Errorf("expected success, got %+v", err) + } + if string(out) != "stdout\n" { + t.Errorf("unexpected output: %q", string(out)) + } + + cmd = ex.Command("/bin/sh", "-c", "echo stderr > /dev/stderr") + out, err = cmd.CombinedOutput() + if err != nil { + t.Errorf("expected success, got %+v", err) + } + if string(out) != "stderr\n" { + t.Errorf("unexpected output: %q", string(out)) + } +}