go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/exec/cmd_test.go (about)

     1  // Copyright 2023 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package exec_test
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"errors"
    21  	"io"
    22  	"os"
    23  	"runtime"
    24  	"testing"
    25  
    26  	. "github.com/smartystreets/goconvey/convey"
    27  	"go.chromium.org/luci/common/exec"
    28  	"go.chromium.org/luci/common/exec/execmock"
    29  	"go.chromium.org/luci/common/system/environ"
    30  	. "go.chromium.org/luci/common/testing/assertions"
    31  )
    32  
    33  var panicRunner = execmock.Register(func(_ execmock.None) (_ execmock.None, exitcode int, err error) {
    34  	panic("big boom")
    35  })
    36  
    37  func TestCmd(t *testing.T) {
    38  	args := []string{"echo", "hello", "there"}
    39  	if runtime.GOOS == "windows" {
    40  		args = append([]string{"cmd.exe", "/c"}, args...)
    41  	}
    42  	fullArgs := args
    43  	var prog string
    44  	prog, args = args[0], args[1:]
    45  
    46  	t.Parallel()
    47  
    48  	Convey(`Cmd`, t, func() {
    49  		ctx := execmock.Init(context.Background())
    50  
    51  		Convey(`works with pasthrough`, func() {
    52  			execmock.Passthrough.Mock(ctx)
    53  
    54  			cmd := exec.Command(ctx, prog, args...)
    55  			data, err := cmd.Output()
    56  			So(err, ShouldBeNil)
    57  			So(bytes.TrimSpace(data), ShouldResemble, []byte("hello there"))
    58  
    59  			cmd = exec.Command(ctx, prog, args...)
    60  			data, err = cmd.CombinedOutput()
    61  			So(err, ShouldBeNil)
    62  			So(bytes.TrimSpace(data), ShouldResemble, []byte("hello there"))
    63  		})
    64  
    65  		Convey(`works with mocking`, func() {
    66  			execmock.Simple.
    67  				WithArgs("echo").
    68  				WithLimit(1).
    69  				Mock(ctx, execmock.SimpleInput{
    70  					Stdout: "not what you expected",
    71  				})
    72  
    73  			cmd := exec.Command(ctx, prog, args...)
    74  			data, err := cmd.CombinedOutput()
    75  			So(err, ShouldBeNil)
    76  			So(string(data), ShouldResemble, "not what you expected")
    77  
    78  			Convey(`and still can't run it twice`, func() {
    79  				So(cmd.Start(), ShouldErrLike, "exec: already started")
    80  			})
    81  		})
    82  
    83  		Convey(`mocking without a fallback generates an error`, func() {
    84  			cmd := exec.CommandContext(ctx, prog, args...)
    85  			So(cmd.Run(), ShouldErrLike, "no mock matches")
    86  		})
    87  
    88  		Convey(`can collect the remaining mocks`, func() {
    89  			echoSingle := execmock.Simple.WithArgs("echo").WithLimit(1)
    90  
    91  			notExpected := echoSingle.Mock(ctx, execmock.SimpleInput{
    92  				Stdout: "not what you expected",
    93  			})
    94  
    95  			different := echoSingle.Mock(ctx, execmock.SimpleInput{
    96  				Stdout: "a different outcome",
    97  			})
    98  
    99  			exit100 := echoSingle.Mock(ctx, execmock.SimpleInput{
   100  				ExitCode: 100,
   101  			})
   102  
   103  			cmd := exec.CommandContext(ctx, prog, args...)
   104  			data, err := cmd.Output()
   105  			So(err, ShouldBeNil)
   106  			So(string(data), ShouldResemble, "not what you expected")
   107  
   108  			So(notExpected.Snapshot(), ShouldHaveLength, 1)
   109  			So(different.Snapshot(), ShouldHaveLength, 0)
   110  			So(exit100.Snapshot(), ShouldHaveLength, 0)
   111  
   112  			cmd = exec.CommandContext(ctx, prog, args...)
   113  			data, err = cmd.Output()
   114  			So(err, ShouldBeNil)
   115  			So(string(data), ShouldResemble, "a different outcome")
   116  
   117  			So(notExpected.Snapshot(), ShouldHaveLength, 1)
   118  			So(different.Snapshot(), ShouldHaveLength, 1)
   119  			So(exit100.Snapshot(), ShouldHaveLength, 0)
   120  
   121  			cmd = exec.CommandContext(ctx, prog, args...)
   122  			_, err = cmd.Output()
   123  			So(err, ShouldErrLike, "exit status 100")
   124  			So(cmd.ProcessState.ExitCode(), ShouldEqual, 100)
   125  
   126  			So(notExpected.Snapshot(), ShouldHaveLength, 1)
   127  			So(different.Snapshot(), ShouldHaveLength, 1)
   128  			So(exit100.Snapshot(), ShouldHaveLength, 1)
   129  		})
   130  
   131  		Convey(`can detect misses`, func() {
   132  			cmd := exec.CommandContext(ctx, prog, args...)
   133  			So(cmd.Run(), ShouldErrLike, "no mock matches")
   134  
   135  			misses := execmock.ResetState(ctx)
   136  			So(misses, ShouldHaveLength, 1)
   137  			So(misses[0].Args, ShouldResemble, fullArgs)
   138  		})
   139  
   140  		Convey(`can simulate a startup error`, func() {
   141  			execmock.StartError.WithArgs("echo").WithLimit(1).Mock(ctx, errors.New("start boom"))
   142  
   143  			cmd := exec.CommandContext(ctx, prog, args...)
   144  			So(cmd.Run(), ShouldErrLike, "start boom")
   145  
   146  			Convey(`which persists`, func() {
   147  				So(cmd.Start(), ShouldErrLike, "start boom")
   148  			})
   149  		})
   150  
   151  		Convey(`can use StdoutPipe`, func() {
   152  			execmock.Simple.Mock(ctx, execmock.SimpleInput{
   153  				Stdout: "hello world",
   154  			})
   155  
   156  			cmd := exec.CommandContext(ctx, prog, args...)
   157  			out, err := cmd.StdoutPipe()
   158  			So(err, ShouldBeNil)
   159  
   160  			So(cmd.Start(), ShouldBeNil)
   161  			data, err := io.ReadAll(out)
   162  			So(cmd.Wait(), ShouldBeNil)
   163  			So(err, ShouldBeNil)
   164  			So(string(data), ShouldResemble, "hello world")
   165  		})
   166  
   167  		Convey(`can use StderrPipe`, func() {
   168  			execmock.Simple.Mock(ctx, execmock.SimpleInput{
   169  				Stderr: "hello world",
   170  			})
   171  
   172  			cmd := exec.CommandContext(ctx, prog, args...)
   173  			out, err := cmd.StderrPipe()
   174  			So(err, ShouldBeNil)
   175  
   176  			So(cmd.Start(), ShouldBeNil)
   177  			data, err := io.ReadAll(out)
   178  			So(cmd.Wait(), ShouldBeNil)
   179  			So(err, ShouldBeNil)
   180  			So(string(data), ShouldResemble, "hello world")
   181  		})
   182  
   183  		Convey(`can see panic`, func() {
   184  			uses := panicRunner.Mock(ctx)
   185  
   186  			cmd := exec.CommandContext(ctx, prog, args...)
   187  			out, err := cmd.StdoutPipe()
   188  			So(err, ShouldBeNil)
   189  
   190  			So(cmd.Start(), ShouldBeNil)
   191  			data, err := io.ReadAll(out)
   192  			So(err, ShouldBeNil)
   193  			So(data, ShouldBeEmpty)
   194  
   195  			So(cmd.Wait(), ShouldErrLike, "exit status 1")
   196  
   197  			_, _, panicStack := uses.Snapshot()[0].GetOutput(context.Background())
   198  			So(panicStack, ShouldNotBeEmpty)
   199  		})
   200  	})
   201  
   202  }
   203  
   204  func TestMain(m *testing.M) {
   205  	if environ.System().Get("EXEC_TEST_SELF_CALL") == "1" {
   206  		os.Stdout.WriteString("EXEC_TEST_SELF_CALL")
   207  		os.Exit(0)
   208  	}
   209  	execmock.Intercept()
   210  	os.Exit(m.Run())
   211  }