go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luciexe/invoke/subprocess_test.go (about) 1 // Copyright 2019 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 invoke 16 17 import ( 18 "context" 19 "flag" 20 "fmt" 21 "io" 22 "os" 23 "os/signal" 24 "path" 25 "testing" 26 "time" 27 28 "google.golang.org/protobuf/proto" 29 "google.golang.org/protobuf/types/known/timestamppb" 30 31 bbpb "go.chromium.org/luci/buildbucket/proto" 32 "go.chromium.org/luci/common/clock" 33 "go.chromium.org/luci/common/clock/testclock" 34 "go.chromium.org/luci/common/system/signals" 35 "go.chromium.org/luci/lucictx" 36 37 . "github.com/smartystreets/goconvey/convey" 38 39 . "go.chromium.org/luci/common/testing/assertions" 40 ) 41 42 const ( 43 selfTestEnvvar = "LUCIEXE_INVOKE_TEST" 44 terminateExitCode = 71 45 unexpectedErrorExitCode = 97 46 ) 47 48 func TestMain(m *testing.M) { 49 switch os.Getenv(selfTestEnvvar) { 50 case "": 51 m.Run() 52 case "exiterr": 53 os.Exit(unexpectedErrorExitCode) 54 case "hang": 55 <-time.After(time.Minute) 56 fmt.Fprintln(os.Stderr, "ERROR: TIMER ENDED") 57 os.Exit(1) 58 case "signal": 59 fmt.Fprintf(os.Stderr, "signal subprocess started\n") 60 signalCh := make(chan os.Signal, 1) 61 signal.Notify(signalCh, signals.Interrupts()...) 62 touch := func(name string) error { 63 f, err := os.OpenFile(name, os.O_RDONLY|os.O_CREATE, 0644) 64 if err != nil { 65 return err 66 } 67 return f.Close() 68 } 69 if err := touch(os.Args[1]); err != nil { 70 fmt.Fprintf(os.Stderr, "ERROR: creating file %s\n", err) 71 os.Exit(unexpectedErrorExitCode) 72 } 73 fmt.Fprintf(os.Stderr, "touched %s\n", os.Args[1]) 74 select { 75 case <-signalCh: 76 os.Exit(terminateExitCode) 77 case <-time.After(time.Minute): 78 fmt.Fprintln(os.Stderr, "ERROR: Timeout waiting for Signal") 79 os.Exit(unexpectedErrorExitCode) 80 } 81 default: 82 out := flag.String("output", "", "write the output here") 83 flag.Parse() 84 85 data, err := io.ReadAll(os.Stdin) 86 if err != nil { 87 panic(err) 88 } 89 90 in := &bbpb.Build{} 91 if err := proto.Unmarshal(data, in); err != nil { 92 panic(err) 93 } 94 in.SummaryMarkdown = "hi" 95 96 if *out != "" { 97 outData, err := proto.Marshal(in) 98 if err != nil { 99 panic(err) 100 } 101 if err := os.WriteFile(*out, outData, 0666); err != nil { 102 panic(err) 103 } 104 } 105 106 os.Exit(0) 107 } 108 } 109 110 func TestSubprocess(t *testing.T) { 111 Convey(`Subprocess`, t, func() { 112 ctx, o, tdir, closer := commonOptions() 113 defer closer() 114 115 o.Env.Set(selfTestEnvvar, "1") 116 117 selfArgs := []string{os.Args[0]} 118 119 Convey(`defaults`, func() { 120 sp, err := Start(ctx, selfArgs, &bbpb.Build{Id: 1}, o) 121 So(err, ShouldBeNil) 122 So(sp.Step, ShouldBeNil) 123 build, err := sp.Wait() 124 So(err, ShouldBeNil) 125 So(build, ShouldResembleProto, &bbpb.Build{}) 126 }) 127 128 Convey(`exiterr`, func() { 129 o.Env.Set(selfTestEnvvar, "exiterr") 130 sp, err := Start(ctx, selfArgs, &bbpb.Build{Id: 1}, o) 131 So(err, ShouldBeNil) 132 So(sp.Step, ShouldBeNil) 133 build, err := sp.Wait() 134 So(err, ShouldErrLike, "exit status 97") 135 So(build, ShouldResembleProto, &bbpb.Build{}) 136 }) 137 138 Convey(`collect`, func() { 139 o.CollectOutput = true 140 sp, err := Start(ctx, selfArgs, &bbpb.Build{Id: 1}, o) 141 So(err, ShouldBeNil) 142 So(sp.Step, ShouldBeNil) 143 build, err := sp.Wait() 144 So(err, ShouldBeNil) 145 So(build, ShouldNotBeNil) 146 So(build.SummaryMarkdown, ShouldEqual, "hi") 147 }) 148 149 Convey(`clear fields in initial build`, func() { 150 o.CollectOutput = true 151 initialBuildTime := time.Date(2020, time.January, 2, 3, 4, 5, 6, time.UTC) 152 ctx, _ := testclock.UseTime(ctx, initialBuildTime) 153 154 inputBuild := &bbpb.Build{ 155 Id: 11, 156 Status: bbpb.Status_CANCELED, 157 StatusDetails: &bbpb.StatusDetails{Timeout: &bbpb.StatusDetails_Timeout{}}, 158 SummaryMarkdown: "Heyo!", 159 EndTime: timestamppb.New(time.Date(2020, time.January, 2, 3, 4, 5, 10, time.UTC)), 160 UpdateTime: timestamppb.New(time.Date(2020, time.January, 2, 3, 4, 5, 11, time.UTC)), 161 Steps: []*bbpb.Step{{Name: "Step cool"}}, 162 Tags: []*bbpb.StringPair{{Key: "foo", Value: "bar"}}, 163 Output: &bbpb.Build_Output{ 164 Logs: []*bbpb.Log{{Name: "stdout"}}, 165 }, 166 } 167 sp, err := Start(ctx, selfArgs, inputBuild, o) 168 So(err, ShouldBeNil) 169 build, err := sp.Wait() 170 So(err, ShouldBeNil) 171 So(build, ShouldResembleProto, &bbpb.Build{ 172 Id: 11, 173 Status: bbpb.Status_STARTED, 174 SummaryMarkdown: "hi", 175 CreateTime: timestamppb.New(initialBuildTime), 176 StartTime: timestamppb.New(initialBuildTime), 177 Tags: []*bbpb.StringPair{{Key: "foo", Value: "bar"}}, 178 }) 179 }) 180 181 Convey(`cancel context`, func() { 182 ctx, cancel := context.WithCancel(ctx) 183 defer cancel() 184 185 start := time.Now() 186 187 o.Env.Set(selfTestEnvvar, "hang") 188 sp, err := Start(ctx, selfArgs, &bbpb.Build{Id: 1}, o) 189 So(err, ShouldBeNil) 190 cancel() 191 _, err = sp.Wait() 192 So(err, ShouldErrLike, "waiting for luciexe") 193 194 So(time.Now(), ShouldHappenWithin, time.Second, start) 195 }) 196 197 Convey(`cancel context before Start`, func() { 198 ctx, cancel := context.WithCancel(ctx) 199 cancel() 200 _, err := Start(ctx, selfArgs, &bbpb.Build{Id: 1}, o) 201 So(err, ShouldErrLike, "prior to starting subprocess: context canceled") 202 }) 203 204 Convey(`deadline`, func() { 205 o.Env.Set(selfTestEnvvar, "signal") 206 207 ctx, tc := testclock.UseTime(ctx, testclock.TestRecentTimeUTC) 208 ctx, cancel := clock.WithTimeout(ctx, 130*time.Second) 209 210 ctx, shutdown := lucictx.TrackSoftDeadline(ctx, 0) 211 defer shutdown() 212 213 readyFile := path.Join(tdir, "readyToCatchSignal") 214 sp, err := Start(ctx, append(selfArgs, readyFile), &bbpb.Build{Id: 1}, o) 215 So(err, ShouldBeNil) 216 timer := time.After(time.Minute) 217 for { 218 select { 219 case <-timer: 220 panic("subprocess is never ready to catch signal") 221 default: 222 _, err = os.Stat(readyFile) 223 } 224 if err == nil { 225 break 226 } else { 227 time.Sleep(time.Second) 228 } 229 } 230 defer os.Remove(readyFile) 231 232 Convey(`interrupt`, func() { 233 shutdown() 234 235 bld, err := sp.Wait() 236 So(err, ShouldContainErr, "luciexe process is interrupted") 237 So(sp.cmd.ProcessState.ExitCode(), ShouldEqual, terminateExitCode) 238 So(bld, ShouldResembleProto, &bbpb.Build{}) 239 }) 240 241 Convey(`timeout`, func() { 242 tc.Add(100 * time.Second) // hits soft deadline 243 244 bld, err := sp.Wait() 245 So(err, ShouldContainErr, "luciexe process timed out") 246 So(sp.cmd.ProcessState.ExitCode(), ShouldEqual, terminateExitCode) 247 So(bld, ShouldResembleProto, &bbpb.Build{ 248 StatusDetails: &bbpb.StatusDetails{Timeout: &bbpb.StatusDetails_Timeout{}}, 249 }) 250 }) 251 252 Convey(`closure`, func() { 253 cancel() 254 255 bld, err := sp.Wait() 256 So(err, ShouldContainErr, "luciexe process's context is cancelled") 257 // The exit code for killed process varies on different platform. 258 So(sp.cmd.ProcessState.ExitCode(), ShouldNotEqual, unexpectedErrorExitCode) 259 So(bld, ShouldResembleProto, &bbpb.Build{}) 260 }) 261 }) 262 }) 263 }