go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luciexe/host/buildmerge/agent_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 buildmerge 16 17 import ( 18 "context" 19 "fmt" 20 "testing" 21 22 "github.com/golang/protobuf/ptypes" 23 "google.golang.org/protobuf/proto" 24 "google.golang.org/protobuf/types/known/structpb" 25 26 bbpb "go.chromium.org/luci/buildbucket/proto" 27 "go.chromium.org/luci/common/clock/testclock" 28 "go.chromium.org/luci/common/proto/reflectutil" 29 "go.chromium.org/luci/logdog/api/logpb" 30 "go.chromium.org/luci/logdog/common/types" 31 "go.chromium.org/luci/luciexe" 32 33 . "github.com/smartystreets/goconvey/convey" 34 35 . "go.chromium.org/luci/common/testing/assertions" 36 ) 37 38 func mkDesc(name string) *logpb.LogStreamDescriptor { 39 return &logpb.LogStreamDescriptor{ 40 Name: name, 41 StreamType: logpb.StreamType_DATAGRAM, 42 ContentType: luciexe.BuildProtoContentType, 43 } 44 } 45 46 func TestAgent(t *testing.T) { 47 t.Parallel() 48 49 Convey(`buildState`, t, func() { 50 now, err := ptypes.TimestampProto(testclock.TestRecentTimeLocal) 51 So(err, ShouldBeNil) 52 ctx, _ := testclock.UseTime(context.Background(), testclock.TestRecentTimeLocal) 53 ctx, cancel := context.WithCancel(ctx) 54 55 baseProps, err := structpb.NewStruct(map[string]any{ 56 "test": "value", 57 }) 58 So(err, ShouldBeNil) 59 60 base := &bbpb.Build{ 61 Input: &bbpb.Build_Input{ 62 Properties: baseProps, 63 }, 64 Output: &bbpb.Build_Output{ 65 Logs: []*bbpb.Log{ 66 {Name: "stdout", Url: "stdout"}, 67 }, 68 }, 69 } 70 // we omit view url here to keep tests simpler 71 merger, err := New(ctx, "u/", base, func(ns, stream types.StreamName) (url, viewURL string) { 72 return fmt.Sprintf("url://%s%s", ns, stream), "" 73 }) 74 So(err, ShouldBeNil) 75 defer merger.Close() 76 defer cancel() 77 78 getFinal := func() (lastBuild *bbpb.Build) { 79 for build := range merger.MergedBuildC { 80 lastBuild = build 81 } 82 return 83 } 84 85 Convey(`can close without any data`, func() { 86 merger.Close() 87 build := <-merger.MergedBuildC 88 89 base.Output.Logs[0].Url = "url://u/stdout" 90 91 So(build, ShouldResembleProto, base) 92 }) 93 94 Convey(`bad stream type`, func() { 95 cb := merger.onNewStream(&logpb.LogStreamDescriptor{ 96 Name: "u/build.proto", 97 StreamType: logpb.StreamType_TEXT, // should be DATAGRAM 98 ContentType: luciexe.BuildProtoContentType, 99 }) 100 So(cb, ShouldBeNil) 101 // NOTE: here and below we do ShouldBeTrue on `ok` instead of using 102 // ShouldNotBeNil on `tracker`. This is because ShouldNotBeNil is 103 // currently (as of Sep'19) implemented in terms of ShouldBeNil, which 104 // ends up traversing the entire `tracker` struct with `reflect`. This 105 // causes the race detector to claim that we're reading the contents of 106 // the atomic.Value in tracker without a lock (which is true). 107 tracker, ok := merger.states["url://u/build.proto"] 108 So(ok, ShouldBeTrue) 109 110 So(tracker.getLatestBuild(), ShouldResembleProto, &bbpb.Build{ 111 EndTime: now, 112 UpdateTime: now, 113 Status: bbpb.Status_INFRA_FAILURE, 114 SummaryMarkdown: "\n\nError in build protocol: build proto stream \"u/build.proto\" has type \"TEXT\", expected \"DATAGRAM\"", 115 Output: &bbpb.Build_Output{ 116 Status: bbpb.Status_INFRA_FAILURE, 117 SummaryMarkdown: "\n\nError in build protocol: build proto stream \"u/build.proto\" has type \"TEXT\", expected \"DATAGRAM\"", 118 }, 119 }) 120 }) 121 122 Convey(`bad content type`, func() { 123 cb := merger.onNewStream(&logpb.LogStreamDescriptor{ 124 Name: "u/build.proto", 125 StreamType: logpb.StreamType_DATAGRAM, 126 ContentType: "i r bad", 127 }) 128 So(cb, ShouldBeNil) 129 tracker, ok := merger.states["url://u/build.proto"] 130 So(ok, ShouldBeTrue) 131 132 So(tracker.getLatestBuild(), ShouldResembleProto, &bbpb.Build{ 133 EndTime: now, 134 UpdateTime: now, 135 Status: bbpb.Status_INFRA_FAILURE, 136 SummaryMarkdown: fmt.Sprintf("\n\nError in build protocol: stream \"u/build.proto\" has content type \"i r bad\", expected one of %v", []string{luciexe.BuildProtoContentType, luciexe.BuildProtoZlibContentType}), 137 Output: &bbpb.Build_Output{ 138 Status: bbpb.Status_INFRA_FAILURE, 139 SummaryMarkdown: fmt.Sprintf("\n\nError in build protocol: stream \"u/build.proto\" has content type \"i r bad\", expected one of %v", []string{luciexe.BuildProtoContentType, luciexe.BuildProtoZlibContentType}), 140 }, 141 }) 142 }) 143 144 Convey(`build.proto suffix but bad stream type and content type `, func() { 145 cb := merger.onNewStream(&logpb.LogStreamDescriptor{ 146 Name: "u/build.proto", 147 StreamType: logpb.StreamType_TEXT, 148 ContentType: "i r bad", 149 }) 150 So(cb, ShouldBeNil) 151 tracker, ok := merger.states["url://u/build.proto"] 152 So(ok, ShouldBeTrue) 153 154 So(tracker.getLatestBuild(), ShouldResembleProto, &bbpb.Build{ 155 EndTime: now, 156 UpdateTime: now, 157 Status: bbpb.Status_INFRA_FAILURE, 158 SummaryMarkdown: fmt.Sprintf("\n\nError in build protocol: build.proto stream \"u/build.proto\" has stream type \"TEXT\" and content type \"i r bad\", expected \"DATAGRAM\" and one of %v", []string{luciexe.BuildProtoContentType, luciexe.BuildProtoZlibContentType}), 159 Output: &bbpb.Build_Output{ 160 Status: bbpb.Status_INFRA_FAILURE, 161 SummaryMarkdown: fmt.Sprintf("\n\nError in build protocol: build.proto stream \"u/build.proto\" has stream type \"TEXT\" and content type \"i r bad\", expected \"DATAGRAM\" and one of %v", []string{luciexe.BuildProtoContentType, luciexe.BuildProtoZlibContentType}), 162 }, 163 }) 164 }) 165 166 Convey(`ignores out-of-namespace streams`, func() { 167 So(merger.onNewStream(&logpb.LogStreamDescriptor{Name: "uprefix"}), ShouldBeNil) 168 So(merger.onNewStream(&logpb.LogStreamDescriptor{Name: "nope/something"}), ShouldBeNil) 169 So(merger.states, ShouldBeEmpty) 170 }) 171 172 Convey(`ignores new registrations on closure`, func() { 173 merger.Close() 174 merger.onNewStream(mkDesc("u/build.proto")) 175 So(merger.states, ShouldBeEmpty) 176 }) 177 178 Convey(`will merge+relay root proto only`, func() { 179 cb := merger.onNewStream(mkDesc("u/build.proto")) 180 So(cb, ShouldNotBeNil) 181 tracker, ok := merger.states["url://u/build.proto"] 182 So(ok, ShouldBeTrue) 183 184 tracker.handleNewData(mkDgram(&bbpb.Build{ 185 Steps: []*bbpb.Step{ 186 {Name: "Hello"}, 187 }, 188 })) 189 190 mergedBuild := <-merger.MergedBuildC 191 expect := reflectutil.ShallowCopy(base).(*bbpb.Build) 192 expect.Steps = append(expect.Steps, &bbpb.Step{Name: "Hello"}) 193 expect.UpdateTime = now 194 expect.Output.Logs[0].Url = "url://u/stdout" 195 So(mergedBuild, ShouldResembleProto, expect) 196 197 merger.Close() 198 <-merger.MergedBuildC // final build 199 }) 200 201 Convey(`can emit changes for merge steps`, func() { 202 merger.onNewStream(mkDesc("u/build.proto")) 203 merger.onNewStream(mkDesc("u/sub/build.proto")) 204 205 rootTrack, ok := merger.states["url://u/build.proto"] 206 So(ok, ShouldBeTrue) 207 subTrack, ok := merger.states["url://u/sub/build.proto"] 208 So(ok, ShouldBeTrue) 209 210 // No merge step yet 211 rootTrack.handleNewData(mkDgram(&bbpb.Build{ 212 Steps: []*bbpb.Step{ 213 {Name: "Hello"}, 214 }, 215 })) 216 expect := reflectutil.ShallowCopy(base).(*bbpb.Build) 217 expect.Steps = append(expect.Steps, &bbpb.Step{Name: "Hello"}) 218 expect.UpdateTime = now 219 expect.Output.Logs[0].Url = "url://u/stdout" 220 So(<-merger.MergedBuildC, ShouldResembleProto, expect) 221 222 // order of updates doesn't matter, so we'll update the sub build first 223 subTrack.handleNewData(mkDgram(&bbpb.Build{ 224 Steps: []*bbpb.Step{ 225 {Name: "SubStep"}, 226 }, 227 })) 228 // the root stream doesn't have the merge step yet, so it doesn't show up. 229 So(<-merger.MergedBuildC, ShouldResembleProto, expect) 230 231 // Ok, now add the merge step 232 rootTrack.handleNewData(mkDgram(&bbpb.Build{ 233 Steps: []*bbpb.Step{ 234 {Name: "Hello"}, 235 {Name: "Merge", 236 MergeBuild: &bbpb.Step_MergeBuild{ 237 FromLogdogStream: "sub/build.proto", 238 }}, 239 }, 240 })) 241 expect.Steps = append(expect.Steps, &bbpb.Step{ 242 Name: "Merge", 243 MergeBuild: &bbpb.Step_MergeBuild{ 244 FromLogdogStream: "url://u/sub/build.proto", 245 }, 246 }) 247 expect.Steps = append(expect.Steps, &bbpb.Step{Name: "Merge|SubStep"}) 248 expect.UpdateTime = now 249 So(<-merger.MergedBuildC, ShouldResembleProto, expect) 250 251 Convey(`and shut down`, func() { 252 merger.Close() 253 expect.EndTime = now 254 expect.Status = bbpb.Status_INFRA_FAILURE 255 expect.Output.Status = bbpb.Status_INFRA_FAILURE 256 expect.SummaryMarkdown = "\n\nError in build protocol: Expected a terminal build status, got STATUS_UNSPECIFIED, while top level status is STATUS_UNSPECIFIED." 257 expect.Output.SummaryMarkdown = expect.SummaryMarkdown 258 for _, step := range expect.Steps { 259 step.EndTime = now 260 if step.Name != "Merge" { 261 step.Status = bbpb.Status_CANCELED 262 step.SummaryMarkdown = "step was never finalized; did the build crash?" 263 } else { 264 step.Status = bbpb.Status_INFRA_FAILURE 265 step.SummaryMarkdown = "\n\nError in build protocol: Expected a terminal build status, got STATUS_UNSPECIFIED, while top level status is STATUS_UNSPECIFIED." 266 } 267 } 268 So(getFinal(), ShouldResembleProto, expect) 269 }) 270 271 Convey(`can handle recursive merge steps`, func() { 272 merger.onNewStream(mkDesc("u/sub/super_deep/build.proto")) 273 superTrack, ok := merger.states["url://u/sub/super_deep/build.proto"] 274 So(ok, ShouldBeTrue) 275 276 subTrack.handleNewData(mkDgram(&bbpb.Build{ 277 Steps: []*bbpb.Step{ 278 {Name: "SubStep"}, 279 {Name: "SuperDeep", 280 MergeBuild: &bbpb.Step_MergeBuild{ 281 FromLogdogStream: "super_deep/build.proto", 282 }}, 283 }, 284 })) 285 <-merger.MergedBuildC // digest subTrack update 286 superTrack.handleNewData(mkDgram(&bbpb.Build{ 287 Steps: []*bbpb.Step{ 288 {Name: "Hi!"}, 289 }, 290 })) 291 expect.Steps = append(expect.Steps, 292 &bbpb.Step{ 293 Name: "Merge|SuperDeep", 294 MergeBuild: &bbpb.Step_MergeBuild{ 295 FromLogdogStream: "url://u/sub/super_deep/build.proto", 296 }, 297 }, 298 &bbpb.Step{ 299 Name: "Merge|SuperDeep|Hi!", 300 }, 301 ) 302 So(<-merger.MergedBuildC, ShouldResembleProto, expect) 303 304 Convey(`and shut down`, func() { 305 merger.Close() 306 307 expect.EndTime = now 308 expect.Status = bbpb.Status_INFRA_FAILURE 309 expect.Output.Status = bbpb.Status_INFRA_FAILURE 310 expect.SummaryMarkdown = "\n\nError in build protocol: Expected a terminal build status, got STATUS_UNSPECIFIED, while top level status is STATUS_UNSPECIFIED." 311 expect.Output.SummaryMarkdown = expect.SummaryMarkdown 312 for _, step := range expect.Steps { 313 step.EndTime = now 314 switch step.Name { 315 case "Merge", "Merge|SuperDeep": 316 step.Status = bbpb.Status_INFRA_FAILURE 317 step.SummaryMarkdown = "\n\nError in build protocol: Expected a terminal build status, got STATUS_UNSPECIFIED, while top level status is STATUS_UNSPECIFIED." 318 default: 319 step.Status = bbpb.Status_CANCELED 320 step.SummaryMarkdown = "step was never finalized; did the build crash?" 321 } 322 } 323 So(getFinal(), ShouldResembleProto, expect) 324 }) 325 }) 326 327 Convey(`and merge sub-build successfully as it becomes invalid`, func() { 328 // added an invalid step to sub build 329 subTrack.handleNewData(mkDgram(&bbpb.Build{ 330 Steps: []*bbpb.Step{ 331 {Name: "SubStep"}, 332 { 333 Name: "Invalid_SubStep", 334 Logs: []*bbpb.Log{ 335 {Url: "emoji 💩 is not a valid url"}, 336 }, 337 }, 338 }, 339 })) 340 341 Convey(`and shut down`, func() { 342 merger.Close() 343 344 expect.EndTime = now 345 expect.Status = bbpb.Status_INFRA_FAILURE 346 expect.Output.Status = bbpb.Status_INFRA_FAILURE 347 expect.SummaryMarkdown = "\n\nError in build protocol: Expected a terminal build status, got STATUS_UNSPECIFIED, while top level status is STATUS_UNSPECIFIED." 348 expect.Output.SummaryMarkdown = expect.SummaryMarkdown 349 expect.Steps = nil 350 expect.Steps = append(expect.Steps, 351 &bbpb.Step{ 352 Name: "Hello", 353 EndTime: now, 354 Status: bbpb.Status_CANCELED, 355 SummaryMarkdown: "step was never finalized; did the build crash?", 356 }, 357 &bbpb.Step{ 358 Name: "Merge", 359 Status: bbpb.Status_INFRA_FAILURE, 360 EndTime: now, 361 MergeBuild: &bbpb.Step_MergeBuild{ 362 FromLogdogStream: "url://u/sub/build.proto", 363 }, 364 SummaryMarkdown: "\n\nError in build protocol: step[\"Invalid_SubStep\"].logs[\"\"]: bad log url \"emoji 💩 is not a valid url\": illegal character ( ) at index 5", 365 }, 366 &bbpb.Step{ 367 Name: "Merge|SubStep", 368 EndTime: now, 369 Status: bbpb.Status_CANCELED, 370 SummaryMarkdown: "step was never finalized; did the build crash?", 371 }, 372 &bbpb.Step{ 373 Name: "Merge|Invalid_SubStep", 374 Status: bbpb.Status_INFRA_FAILURE, 375 EndTime: now, 376 Logs: []*bbpb.Log{ 377 {Url: "emoji 💩 is not a valid url"}, 378 }, 379 SummaryMarkdown: "bad log url \"emoji 💩 is not a valid url\": illegal character ( ) at index 5", 380 }, 381 ) 382 So(getFinal(), ShouldResembleProto, expect) 383 }) 384 }) 385 }) 386 387 Convey(`can merge sub-build`, func() { 388 merger.onNewStream(mkDesc("u/build.proto")) 389 rootTrack, ok := merger.states["url://u/build.proto"] 390 So(ok, ShouldBeTrue) 391 392 rootTrack.handleNewData(mkDgram(&bbpb.Build{ 393 Steps: []*bbpb.Step{ 394 { 395 Name: "Merge", 396 Status: bbpb.Status_STARTED, 397 MergeBuild: &bbpb.Step_MergeBuild{ 398 FromLogdogStream: "sub/build.proto", 399 }, 400 }, 401 }, 402 })) 403 404 expect := proto.Clone(base).(*bbpb.Build) 405 expect.Steps = nil 406 expect.UpdateTime = now 407 expect.Output.Logs[0].Url = "url://u/stdout" 408 409 Convey(`when sub-build stream has not been registered yet`, func() { 410 expect.Steps = []*bbpb.Step{ 411 { 412 Name: "Merge", 413 Status: bbpb.Status_STARTED, 414 MergeBuild: &bbpb.Step_MergeBuild{ 415 FromLogdogStream: "url://u/sub/build.proto", 416 }, 417 SummaryMarkdown: "build.proto stream: \"url://u/sub/build.proto\" is not registered", 418 }, 419 } 420 So(<-merger.MergedBuildC, ShouldResembleProto, expect) 421 422 Convey(`Append existing SummaryMarkdown`, func() { 423 rootTrack.handleNewData(mkDgram(&bbpb.Build{ 424 Steps: []*bbpb.Step{ 425 { 426 Name: "Merge", 427 Status: bbpb.Status_STARTED, 428 SummaryMarkdown: "existing summary", 429 MergeBuild: &bbpb.Step_MergeBuild{ 430 FromLogdogStream: "sub/build.proto", 431 }, 432 }, 433 }, 434 })) 435 436 expect.Steps = []*bbpb.Step{ 437 { 438 Name: "Merge", 439 Status: bbpb.Status_STARTED, 440 MergeBuild: &bbpb.Step_MergeBuild{ 441 FromLogdogStream: "url://u/sub/build.proto", 442 }, 443 SummaryMarkdown: "existing summary\n\nbuild.proto stream: \"url://u/sub/build.proto\" is not registered", 444 }, 445 } 446 So(<-merger.MergedBuildC, ShouldResembleProto, expect) 447 }) 448 449 Convey(`then registered but stream is empty`, func() { 450 merger.onNewStream(mkDesc("u/sub/build.proto")) 451 subTrack, ok := merger.states["url://u/sub/build.proto"] 452 So(ok, ShouldBeTrue) 453 expect.Steps = []*bbpb.Step{ 454 { 455 Name: "Merge", 456 Status: bbpb.Status_STARTED, 457 MergeBuild: &bbpb.Step_MergeBuild{ 458 FromLogdogStream: "url://u/sub/build.proto", 459 }, 460 SummaryMarkdown: "build.proto stream: \"url://u/sub/build.proto\" is empty", 461 }, 462 } 463 // send something random to kick off a merge. 464 merger.onNewStream(mkDesc("u/unknown/build.proto"))(mkDgram(&bbpb.Build{})) 465 So(<-merger.MergedBuildC, ShouldResembleProto, expect) 466 467 Convey(`finally merge properly when sub-build stream is present`, func() { 468 subTrack.handleNewData(mkDgram(&bbpb.Build{ 469 Status: bbpb.Status_SUCCESS, 470 Output: &bbpb.Build_Output{ 471 Status: bbpb.Status_SUCCESS, 472 }, 473 Steps: []*bbpb.Step{ 474 {Name: "SubStep"}, 475 }, 476 })) 477 expect.Steps = []*bbpb.Step{ 478 { 479 Name: "Merge", 480 Status: bbpb.Status_SUCCESS, 481 MergeBuild: &bbpb.Step_MergeBuild{ 482 FromLogdogStream: "url://u/sub/build.proto", 483 }, 484 }, 485 {Name: "Merge|SubStep"}, 486 } 487 So(<-merger.MergedBuildC, ShouldResembleProto, expect) 488 }) 489 490 }) 491 }) 492 }) 493 494 Convey(`can merge sub-build into global namespace`, func() { 495 merger.onNewStream(mkDesc("u/build.proto")) 496 rootTrack, ok := merger.states["url://u/build.proto"] 497 So(ok, ShouldBeTrue) 498 499 baseProps, err := structpb.NewStruct(map[string]any{ 500 "something": "value", 501 }) 502 So(err, ShouldBeNil) 503 504 rootTrack.handleNewData(mkDgram(&bbpb.Build{ 505 Output: &bbpb.Build_Output{ 506 Properties: baseProps, 507 }, 508 SummaryMarkdown: "some words", 509 Steps: []*bbpb.Step{ 510 { 511 Name: "Merge", 512 Status: bbpb.Status_STARTED, 513 MergeBuild: &bbpb.Step_MergeBuild{ 514 FromLogdogStream: "sub/build.proto", 515 LegacyGlobalNamespace: true, 516 }, 517 }, 518 }, 519 })) 520 // make sure to pull this through to avoid races 521 <-merger.MergedBuildC 522 523 expect := proto.Clone(base).(*bbpb.Build) 524 expect.Steps = nil 525 expect.UpdateTime = now 526 expect.SummaryMarkdown = "some words" 527 expect.Output.Logs[0].Url = "url://u/stdout" 528 expect.Output.Properties, _ = structpb.NewStruct(map[string]any{ 529 "something": "value", 530 }) 531 expect.Steps = []*bbpb.Step{ 532 { 533 Name: "Merge", 534 Status: bbpb.Status_STARTED, 535 MergeBuild: &bbpb.Step_MergeBuild{ 536 FromLogdogStream: "url://u/sub/build.proto", 537 LegacyGlobalNamespace: true, 538 }, 539 SummaryMarkdown: "build.proto stream: \"url://u/sub/build.proto\" is empty", 540 }, 541 } 542 543 merger.onNewStream(mkDesc("u/sub/build.proto")) 544 subTrack, ok := merger.states["url://u/sub/build.proto"] 545 So(ok, ShouldBeTrue) 546 547 Convey(`Overwrites properties`, func() { 548 subProps, err := structpb.NewStruct(map[string]any{ 549 "new": "prop", 550 "something": "overwrite", 551 }) 552 So(err, ShouldBeNil) 553 subTrack.handleNewData(mkDgram(&bbpb.Build{ 554 Output: &bbpb.Build_Output{ 555 Properties: subProps, 556 Status: bbpb.Status_STARTED, 557 }, 558 Status: bbpb.Status_STARTED, 559 Steps: []*bbpb.Step{ 560 {Name: "SubStep"}, 561 }, 562 })) 563 expect.Steps = append(expect.Steps, &bbpb.Step{Name: "SubStep"}) 564 expect.Output.Properties.Fields["new"] = structpb.NewStringValue("prop") 565 expect.Output.Properties.Fields["something"] = structpb.NewStringValue("overwrite") 566 expect.Steps[0].SummaryMarkdown = "" 567 So(<-merger.MergedBuildC, ShouldResembleProto, expect) 568 }) 569 570 }) 571 }) 572 }