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 }