go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luciexe/invoke/options_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 "io/ioutil" 20 "os" 21 "path/filepath" 22 "runtime" 23 "testing" 24 "time" 25 26 "github.com/golang/protobuf/jsonpb" 27 "google.golang.org/protobuf/proto" 28 "google.golang.org/protobuf/types/known/timestamppb" 29 30 bbpb "go.chromium.org/luci/buildbucket/proto" 31 "go.chromium.org/luci/common/clock" 32 "go.chromium.org/luci/common/clock/testclock" 33 "go.chromium.org/luci/common/data/stringset" 34 "go.chromium.org/luci/common/system/environ" 35 "go.chromium.org/luci/logdog/client/butlerlib/bootstrap" 36 "go.chromium.org/luci/lucictx" 37 "go.chromium.org/luci/luciexe" 38 39 . "github.com/smartystreets/goconvey/convey" 40 41 . "go.chromium.org/luci/common/testing/assertions" 42 ) 43 44 var tempEnvVar string 45 46 func init() { 47 if runtime.GOOS == "windows" { 48 tempEnvVar = "TMP" 49 } else { 50 tempEnvVar = "TMPDIR" 51 } 52 } 53 54 var nullLogdogEnv = environ.New([]string{ 55 bootstrap.EnvStreamServerPath + "=null", 56 bootstrap.EnvStreamProject + "=testing", 57 bootstrap.EnvStreamPrefix + "=prefix", 58 bootstrap.EnvCoordinatorHost + "=test.example.com", 59 }) 60 61 func commonOptions() (ctx context.Context, o *Options, tdir string, closer func()) { 62 ctx, _ = testclock.UseTime(context.Background(), testclock.TestRecentTimeUTC) 63 ctx = lucictx.SetDeadline(ctx, nil) 64 65 // luciexe protocol requires the 'host' application to manage the tempdir. 66 // In this context the test binary is the host. It's more 67 // convenient+accurate to have a non-hermetic test than to mock this out. 68 oldTemp := os.Getenv(tempEnvVar) 69 closer = func() { 70 if err := os.Setenv(tempEnvVar, oldTemp); err != nil { 71 panic(err) 72 } 73 if tdir != "" { 74 So(os.RemoveAll(tdir), ShouldBeNil) 75 } 76 } 77 78 var err error 79 if tdir, err = ioutil.TempDir("", "luciexe_test"); err != nil { 80 closer() // want to do cleanup if ioutil.TempDir failed 81 So(err, ShouldBeNil) 82 } 83 if err := os.Setenv(tempEnvVar, tdir); err != nil { 84 panic(err) 85 } 86 87 o = &Options{ 88 Env: nullLogdogEnv.Clone(), 89 } 90 91 return 92 } 93 94 func TestOptionsGeneral(t *testing.T) { 95 Convey(`test Options (general)`, t, func() { 96 ctx, _ := testclock.UseTime(context.Background(), testclock.TestRecentTimeUTC) 97 98 Convey(`works with nil`, func() { 99 // TODO(iannucci): really gotta put all these envvars in LUCI_CONTEXT... 100 oldVals := map[string]string{} 101 for k, v := range nullLogdogEnv.Map() { 102 oldVals[k] = os.Getenv(k) 103 if err := os.Setenv(k, v); err != nil { 104 panic(err) 105 } 106 } 107 defer func() { 108 for k, v := range oldVals { 109 if err := os.Setenv(k, v); err != nil { 110 panic(err) 111 } 112 } 113 }() 114 expected := stringset.NewFromSlice(luciexe.TempDirEnvVars...) 115 for key := range environ.System().Map() { 116 expected.Add(key) 117 } 118 expected.Add(lucictx.EnvKey) 119 120 lo, _, err := ((*Options)(nil)).rationalize(ctx) 121 So(err, ShouldBeNil) 122 123 envKeys := stringset.New(expected.Len()) 124 lo.env.Iter(func(k, _ string) error { 125 envKeys.Add(k) 126 return nil 127 }) 128 So(envKeys, ShouldResemble, expected) 129 }) 130 }) 131 } 132 133 func TestOptionsNamespace(t *testing.T) { 134 Convey(`test Options.Namespace`, t, func() { 135 ctx, o, _, closer := commonOptions() 136 defer closer() 137 138 nowP := timestamppb.New(clock.Now(ctx)) 139 140 Convey(`default`, func() { 141 lo, _, err := o.rationalize(ctx) 142 So(err, ShouldBeNil) 143 So(lo.env.Get(bootstrap.EnvNamespace), ShouldResemble, "") 144 So(lo.step, ShouldBeNil) 145 }) 146 147 Convey(`errors`, func() { 148 Convey(`bad clock`, func() { 149 o.Namespace = "yarp" 150 ctx, _ := testclock.UseTime(ctx, time.Unix(-100000000000, 0)) 151 152 _, _, err := o.rationalize(ctx) 153 So(err, ShouldErrLike, "preparing namespace: invalid StartTime") 154 }) 155 }) 156 157 Convey(`toplevel`, func() { 158 o.Namespace = "u" 159 lo, _, err := o.rationalize(ctx) 160 So(err, ShouldBeNil) 161 So(lo.env.Get(bootstrap.EnvNamespace), ShouldResemble, "u") 162 So(lo.step, ShouldResembleProto, &bbpb.Step{ 163 Name: "u", 164 StartTime: nowP, 165 Status: bbpb.Status_STARTED, 166 Logs: []*bbpb.Log{ 167 {Name: "stdout", Url: "u/stdout"}, 168 {Name: "stderr", Url: "u/stderr"}, 169 }, 170 MergeBuild: &bbpb.Step_MergeBuild{ 171 FromLogdogStream: "u/build.proto", 172 }, 173 }) 174 }) 175 176 Convey(`nested`, func() { 177 o.Env.Set(bootstrap.EnvNamespace, "u/bar") 178 o.Namespace = "sub" 179 lo, _, err := o.rationalize(ctx) 180 So(err, ShouldBeNil) 181 So(lo.env.Get(bootstrap.EnvNamespace), ShouldResemble, "u/bar/sub") 182 So(lo.step, ShouldResembleProto, &bbpb.Step{ 183 Name: "sub", // host application will swizzle this 184 StartTime: nowP, 185 Status: bbpb.Status_STARTED, 186 Logs: []*bbpb.Log{ 187 {Name: "stdout", Url: "sub/stdout"}, 188 {Name: "stderr", Url: "sub/stderr"}, 189 }, 190 MergeBuild: &bbpb.Step_MergeBuild{ 191 FromLogdogStream: "sub/build.proto", 192 }, 193 }) 194 }) 195 196 Convey(`deeply nested`, func() { 197 o.Env.Set(bootstrap.EnvNamespace, "u") 198 o.Namespace = "step|!!cool!!|sub" 199 lo, _, err := o.rationalize(ctx) 200 So(err, ShouldBeNil) 201 So(lo.env.Get(bootstrap.EnvNamespace), 202 ShouldResemble, "u/step/s___cool__/sub") 203 So(lo.step, ShouldResembleProto, &bbpb.Step{ 204 Name: "step|!!cool!!|sub", // host application will swizzle this 205 StartTime: nowP, 206 Status: bbpb.Status_STARTED, 207 Logs: []*bbpb.Log{ 208 {Name: "stdout", Url: "step/s___cool__/sub/stdout"}, 209 {Name: "stderr", Url: "step/s___cool__/sub/stderr"}, 210 }, 211 MergeBuild: &bbpb.Step_MergeBuild{ 212 FromLogdogStream: "step/s___cool__/sub/build.proto", 213 }, 214 }) 215 }) 216 }) 217 } 218 219 func TestOptionsCacheDir(t *testing.T) { 220 Convey(`Options.CacheDir`, t, func() { 221 ctx, o, tdir, closer := commonOptions() 222 defer closer() 223 224 Convey(`default`, func() { 225 _, ctx, err := o.rationalize(ctx) 226 So(err, ShouldBeNil) 227 lexe := lucictx.GetLUCIExe(ctx) 228 So(lexe, ShouldNotBeNil) 229 So(lexe.CacheDir, ShouldStartWith, tdir) 230 }) 231 232 Convey(`override`, func() { 233 o.CacheDir = filepath.Join(tdir, "cache") 234 So(os.Mkdir(o.CacheDir, 0777), ShouldBeNil) 235 _, ctx, err := o.rationalize(ctx) 236 So(err, ShouldBeNil) 237 lexe := lucictx.GetLUCIExe(ctx) 238 So(lexe, ShouldNotBeNil) 239 So(lexe.CacheDir, ShouldEqual, o.CacheDir) 240 }) 241 242 Convey(`errors`, func() { 243 Convey(`empty cache dir set`, func() { 244 ctx := lucictx.SetLUCIExe(ctx, &lucictx.LUCIExe{}) 245 _, _, err := o.rationalize(ctx) 246 So(err, ShouldErrLike, `"cache_dir" is empty`) 247 }) 248 249 Convey(`bad override (doesn't exist)`, func() { 250 o.CacheDir = filepath.Join(tdir, "cache") 251 _, _, err := o.rationalize(ctx) 252 So(err, ShouldErrLike, "checking CacheDir: dir does not exist") 253 }) 254 255 Convey(`bad override (not a dir)`, func() { 256 o.CacheDir = filepath.Join(tdir, "cache") 257 So(os.WriteFile(o.CacheDir, []byte("not a dir"), 0666), ShouldBeNil) 258 _, _, err := o.rationalize(ctx) 259 So(err, ShouldErrLike, "checking CacheDir: path is not a directory") 260 }) 261 }) 262 }) 263 } 264 265 func TestOptionsCollectOutput(t *testing.T) { 266 Convey(`Options.CollectOutput`, t, func() { 267 ctx, o, tdir, closer := commonOptions() 268 defer closer() 269 270 Convey(`default`, func() { 271 lo, _, err := o.rationalize(ctx) 272 So(err, ShouldBeNil) 273 So(lo.args, ShouldBeEmpty) 274 out, err := luciexe.ReadBuildFile(lo.collectPath) 275 So(err, ShouldBeNil) 276 So(out, ShouldBeNil) 277 }) 278 279 Convey(`errors`, func() { 280 Convey(`bad extension`, func() { 281 o.CollectOutputPath = filepath.Join(tdir, "output.fleem") 282 _, _, err := o.rationalize(ctx) 283 So(err, ShouldErrLike, "bad extension for build proto file path") 284 }) 285 286 Convey(`already exists`, func() { 287 outPath := filepath.Join(tdir, "output.pb") 288 o.CollectOutputPath = outPath 289 So(os.WriteFile(outPath, nil, 0666), ShouldBeNil) 290 _, _, err := o.rationalize(ctx) 291 So(err, ShouldErrLike, "CollectOutputPath points to an existing file") 292 }) 293 294 Convey(`parent is not a dir`, func() { 295 parDir := filepath.Join(tdir, "parent") 296 So(os.WriteFile(parDir, nil, 0666), ShouldBeNil) 297 o.CollectOutputPath = filepath.Join(parDir, "out.pb") 298 299 _, _, err := o.rationalize(ctx) 300 So(err, ShouldErrLike, "checking CollectOutputPath's parent: path is not a directory") 301 }) 302 303 Convey(`no parent folder`, func() { 304 o.CollectOutputPath = filepath.Join(tdir, "extra", "output.fleem") 305 _, _, err := o.rationalize(ctx) 306 So(err, ShouldErrLike, "checking CollectOutputPath's parent: dir does not exist") 307 }) 308 }) 309 310 Convey(`parseOutput`, func() { 311 expected := &bbpb.Build{SummaryMarkdown: "I'm a summary."} 312 testParseOutput := func(expectedData []byte, checkFilename func(string)) { 313 lo, _, err := o.rationalize(ctx) 314 So(err, ShouldBeNil) 315 So(lo.args, ShouldHaveLength, 2) 316 So(lo.args[0], ShouldEqual, luciexe.OutputCLIArg) 317 checkFilename(lo.args[1]) 318 319 _, err = luciexe.ReadBuildFile(lo.collectPath) 320 So(err, ShouldErrLike, "opening build file") 321 322 So(os.WriteFile(lo.args[1], expectedData, 0666), ShouldBeNil) 323 324 build, err := luciexe.ReadBuildFile(lo.collectPath) 325 So(err, ShouldBeNil) 326 So(build, ShouldResembleProto, expected) 327 } 328 329 Convey(`collect but no specific file`, func() { 330 o.CollectOutput = true 331 data, err := proto.Marshal(expected) 332 So(err, ShouldBeNil) 333 testParseOutput(data, func(filename string) { 334 So(filename, ShouldStartWith, tdir) 335 So(filename, ShouldEndWith, luciexe.BuildFileCodecBinary.FileExtension()) 336 }) 337 }) 338 339 Convey(`collect from a binary file`, func() { 340 o.CollectOutput = true 341 outPath := filepath.Join(tdir, "output.pb") 342 o.CollectOutputPath = outPath 343 344 data, err := proto.Marshal(expected) 345 So(err, ShouldBeNil) 346 347 testParseOutput(data, func(filename string) { 348 So(filename, ShouldEqual, outPath) 349 }) 350 }) 351 352 Convey(`collect from a json file`, func() { 353 o.CollectOutput = true 354 outPath := filepath.Join(tdir, "output.json") 355 o.CollectOutputPath = outPath 356 357 data, err := (&jsonpb.Marshaler{OrigName: true}).MarshalToString(expected) 358 So(err, ShouldBeNil) 359 360 testParseOutput([]byte(data), func(filename string) { 361 So(filename, ShouldEqual, outPath) 362 }) 363 }) 364 365 Convey(`collect from a textpb file`, func() { 366 o.CollectOutput = true 367 outPath := filepath.Join(tdir, "output.textpb") 368 o.CollectOutputPath = outPath 369 370 testParseOutput([]byte(expected.String()), func(filename string) { 371 So(filename, ShouldEqual, outPath) 372 }) 373 }) 374 }) 375 }) 376 } 377 378 func TestOptionsEnv(t *testing.T) { 379 Convey(`Env`, t, func() { 380 ctx, o, _, closer := commonOptions() 381 defer closer() 382 383 Convey(`default`, func() { 384 lo, _, err := o.rationalize(ctx) 385 So(err, ShouldBeNil) 386 387 expected := stringset.NewFromSlice(luciexe.TempDirEnvVars...) 388 for key := range o.Env.Map() { 389 expected.Add(key) 390 } 391 expected.Add(lucictx.EnvKey) 392 393 actual := stringset.New(expected.Len()) 394 for key := range lo.env.Map() { 395 actual.Add(key) 396 } 397 398 So(actual, ShouldResemble, expected) 399 }) 400 }) 401 } 402 403 func TestOptionsStdio(t *testing.T) { 404 Convey(`stdio`, t, func() { 405 ctx, o, _, closer := commonOptions() 406 defer closer() 407 408 Convey(`default`, func() { 409 lo, _, err := o.rationalize(ctx) 410 So(err, ShouldBeNil) 411 So(lo.stderr, ShouldNotBeNil) 412 So(lo.stdout, ShouldNotBeNil) 413 }) 414 415 Convey(`errors`, func() { 416 Convey(`bad bootstrap (missing)`, func() { 417 o.Env.Remove(bootstrap.EnvStreamServerPath) 418 _, _, err := o.rationalize(ctx) 419 So(err, ShouldErrLike, "Logdog Butler environment required") 420 }) 421 422 Convey(`bad bootstrap (malformed)`, func() { 423 o.Env.Set(bootstrap.EnvStreamPrefix, "!!!!") 424 _, _, err := o.rationalize(ctx) 425 So(err, ShouldErrLike, `failed to validate prefix "!!!!"`) 426 }) 427 }) 428 }) 429 } 430 431 func TestOptionsExtraDirs(t *testing.T) { 432 Convey(`tempDir+workDir`, t, func() { 433 ctx, o, tdir, closer := commonOptions() 434 defer closer() 435 436 Convey(`provided BaseDir`, func() { 437 o.BaseDir = filepath.Join(tdir, "base") 438 So(os.Mkdir(o.BaseDir, 0777), ShouldBeNil) 439 lo, _, err := o.rationalize(ctx) 440 So(err, ShouldBeNil) 441 So(lo.env.Get("TMP"), ShouldStartWith, o.BaseDir) 442 So(lo.workDir, ShouldStartWith, o.BaseDir) 443 }) 444 445 Convey(`provided BaseDir does not exist`, func() { 446 o.BaseDir = filepath.Join(tdir, "base") 447 _, _, err := o.rationalize(ctx) 448 So(err, ShouldErrLike, "checking BaseDir: dir does not exist") 449 }) 450 451 Convey(`provided BaseDir is not a directory`, func() { 452 o.BaseDir = filepath.Join(tdir, "base") 453 So(os.WriteFile(o.BaseDir, []byte("not a dir"), 0666), ShouldBeNil) 454 _, _, err := o.rationalize(ctx) 455 So(err, ShouldErrLike, "checking BaseDir: path is not a directory") 456 }) 457 458 Convey(`fallback to temp`, func() { 459 lo, _, err := o.rationalize(ctx) 460 So(err, ShouldBeNil) 461 So(lo.env.Get("TMP"), ShouldStartWith, tdir) 462 So(lo.workDir, ShouldStartWith, tdir) 463 }) 464 }) 465 }