go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luciexe/host/buildmerge/build_state_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 "errors" 20 "fmt" 21 "regexp" 22 "testing" 23 24 "github.com/golang/protobuf/ptypes" 25 "google.golang.org/protobuf/proto" 26 27 bbpb "go.chromium.org/luci/buildbucket/proto" 28 "go.chromium.org/luci/common/clock/testclock" 29 "go.chromium.org/luci/logdog/api/logpb" 30 "go.chromium.org/luci/logdog/common/types" 31 32 . "github.com/smartystreets/goconvey/convey" 33 34 . "go.chromium.org/luci/common/testing/assertions" 35 ) 36 37 func mkDgram(build *bbpb.Build) *logpb.LogEntry { 38 data, err := proto.Marshal(build) 39 So(err, ShouldBeNil) 40 return &logpb.LogEntry{ 41 Content: &logpb.LogEntry_Datagram{Datagram: &logpb.Datagram{ 42 Data: data, 43 }}, 44 } 45 } 46 47 // cleanupProtoError inspects the SummaryMarkdown field of build proto 48 // and if it contains proto err, it will strip out the proto error message. 49 // The reason we are doing this is that the proto lib discourage us to perform 50 // error string comparison. See: 51 // https://github.com/protocolbuffers/protobuf-go/blob/cd108d00a8df3bba55927ef35ca07438b895d7aa/internal/errors/errors.go#L26-L34 52 func cleanupProtoError(builds ...*bbpb.Build) { 53 re := regexp.MustCompile(`Error in build protocol:.*\sproto:`) 54 for _, build := range builds { 55 if build.Output.GetSummaryMarkdown() != "" { 56 So(build.Output.SummaryMarkdown, ShouldEqual, build.SummaryMarkdown) 57 } 58 if loc := re.FindStringIndex(build.SummaryMarkdown); loc != nil { 59 build.SummaryMarkdown = build.SummaryMarkdown[:loc[1]] 60 if build.Output.GetSummaryMarkdown() != "" { 61 build.Output.SummaryMarkdown = build.SummaryMarkdown 62 } 63 } 64 } 65 } 66 67 func assertStateEqual(actual, expected *buildState) { 68 cleanupProtoError(actual.build, expected.build) 69 So(actual.build, ShouldResembleProto, expected.build) 70 So(actual.closed, ShouldEqual, expected.closed) 71 So(actual.final, ShouldEqual, expected.final) 72 So(actual.invalid, ShouldEqual, expected.invalid) 73 } 74 75 func TestBuildState(t *testing.T) { 76 t.Parallel() 77 78 Convey(`buildState`, t, func() { 79 now, err := ptypes.TimestampProto(testclock.TestRecentTimeLocal) 80 So(err, ShouldBeNil) 81 ctx, _ := testclock.UseTime(context.Background(), testclock.TestRecentTimeLocal) 82 ctx, cancel := context.WithCancel(ctx) 83 defer cancel() 84 85 merger, err := New(ctx, "u/", &bbpb.Build{Output: &bbpb.Build_Output{}}, func(ns, stream types.StreamName) (url, viewURL string) { 86 return fmt.Sprintf("url://%s%s", ns, stream), fmt.Sprintf("view://%s%s", ns, stream) 87 }) 88 So(err, ShouldBeNil) 89 defer merger.Close() 90 91 informChan := make(chan struct{}, 1) 92 merger.informNewData = func() { 93 informChan <- struct{}{} 94 } 95 wait := func() { 96 <-informChan 97 } 98 99 Convey(`opened in error state`, func() { 100 bs := newBuildStateTracker(ctx, merger, "ns/", false, errors.New("nope")) 101 wait() // for final build 102 So(bs.workClosed, ShouldBeTrue) 103 bs.Drain() 104 assertStateEqual(bs.latestState, &buildState{ 105 build: &bbpb.Build{ 106 SummaryMarkdown: "\n\nError in build protocol: nope", 107 Status: bbpb.Status_INFRA_FAILURE, 108 UpdateTime: now, 109 EndTime: now, 110 Output: &bbpb.Build_Output{ 111 Status: bbpb.Status_INFRA_FAILURE, 112 SummaryMarkdown: "\n\nError in build protocol: nope", 113 }, 114 }, 115 closed: true, 116 final: true, 117 }) 118 }) 119 120 Convey(`basic`, func() { 121 bs := newBuildStateTracker(ctx, merger, "ns/", false, nil) 122 123 Convey(`ignores updates when merger cancels context`, func() { 124 cancel() 125 wait() // cancel generated an 'informNewData' 126 127 bs.handleNewData(mkDgram(&bbpb.Build{ 128 SummaryMarkdown: "some stuff", 129 Steps: []*bbpb.Step{ 130 {Name: "Parent"}, 131 {Name: "Parent|Child"}, 132 { 133 Name: "Parent|Merge", 134 Logs: []*bbpb.Log{{ 135 Name: "$build.proto", 136 Url: "Parent/Merge/build.proto", 137 }}, 138 }, 139 }, 140 })) 141 // No wait, because this handleNewData was ignored. 142 bs.Drain() 143 assertStateEqual(bs.latestState, &buildState{ 144 build: &bbpb.Build{ 145 EndTime: now, 146 UpdateTime: now, 147 Status: bbpb.Status_INFRA_FAILURE, 148 SummaryMarkdown: "Never received any build data.", 149 Output: &bbpb.Build_Output{ 150 Status: bbpb.Status_INFRA_FAILURE, 151 SummaryMarkdown: "Never received any build data.", 152 }, 153 }, 154 closed: true, 155 final: true, 156 }) 157 158 Convey(`can still close, though`, func() { 159 bs.handleNewData(nil) 160 bs.Drain() 161 assertStateEqual(bs.latestState, &buildState{ 162 closed: true, 163 final: true, 164 build: &bbpb.Build{ 165 EndTime: now, 166 UpdateTime: now, 167 Status: bbpb.Status_INFRA_FAILURE, 168 SummaryMarkdown: "Never received any build data.", 169 Output: &bbpb.Build_Output{ 170 Status: bbpb.Status_INFRA_FAILURE, 171 SummaryMarkdown: "Never received any build data.", 172 }, 173 }, 174 }) 175 }) 176 }) 177 178 Convey(`no updates`, func() { 179 Convey(`handleNewData(nil)`, func() { 180 bs.handleNewData(nil) 181 wait() 182 bs.Drain() 183 assertStateEqual(bs.latestState, &buildState{ 184 closed: true, 185 final: true, 186 build: &bbpb.Build{ 187 EndTime: now, 188 UpdateTime: now, 189 Status: bbpb.Status_INFRA_FAILURE, 190 SummaryMarkdown: "Never received any build data.", 191 Output: &bbpb.Build_Output{ 192 Status: bbpb.Status_INFRA_FAILURE, 193 SummaryMarkdown: "Never received any build data.", 194 }, 195 }}, 196 ) 197 198 Convey(`convey close noop`, func() { 199 // not a valid state, but bs is closed, so handleNewData should do nothing 200 // when invoked. 201 bs.latestState = &buildState{ 202 closed: true, 203 final: true, 204 build: &bbpb.Build{SummaryMarkdown: "wat"}, 205 } 206 bs.handleNewData(nil) 207 assertStateEqual(bs.latestState, &buildState{ 208 closed: true, 209 final: true, 210 build: &bbpb.Build{SummaryMarkdown: "wat"}, 211 }) 212 }) 213 }) 214 }) 215 216 Convey(`valid update`, func() { 217 bs.handleNewData(mkDgram(&bbpb.Build{ 218 SummaryMarkdown: "some stuff", 219 Steps: []*bbpb.Step{ 220 {Name: "Parent"}, 221 {Name: "Parent|Child"}, 222 { 223 Name: "Parent|Merge", 224 Logs: []*bbpb.Log{{ 225 Name: "$build.proto", 226 Url: "Parent/Merge/build.proto", 227 }}, 228 }, 229 }, 230 Output: &bbpb.Build_Output{ 231 Logs: []*bbpb.Log{{ 232 Name: "stderr", 233 Url: "stderr", 234 }}, 235 }, 236 })) 237 wait() 238 239 So(bs.getLatestBuild(), ShouldResembleProto, &bbpb.Build{ 240 SummaryMarkdown: "some stuff", 241 Steps: []*bbpb.Step{ 242 {Name: "Parent"}, 243 {Name: "Parent|Child"}, 244 { 245 Name: "Parent|Merge", 246 Logs: []*bbpb.Log{}, 247 MergeBuild: &bbpb.Step_MergeBuild{ 248 FromLogdogStream: "url://ns/Parent/Merge/build.proto", 249 }, 250 }, 251 }, 252 UpdateTime: now, 253 Output: &bbpb.Build_Output{ 254 Logs: []*bbpb.Log{{ 255 Name: "stderr", 256 Url: "url://ns/stderr", 257 ViewUrl: "view://ns/stderr", 258 }}, 259 }, 260 }) 261 262 Convey(`followed by garbage`, func() { 263 bs.handleNewData(&logpb.LogEntry{ 264 Content: &logpb.LogEntry_Datagram{Datagram: &logpb.Datagram{ 265 Data: []byte("narpnarp"), 266 }}, 267 }) 268 wait() 269 wait() // for final build 270 bs.Drain() 271 assertStateEqual(bs.latestState, &buildState{ 272 closed: true, 273 final: true, 274 invalid: true, 275 build: &bbpb.Build{ 276 SummaryMarkdown: ("some stuff\n\n" + 277 "Error in build protocol: parsing Build: proto:"), 278 Status: bbpb.Status_INFRA_FAILURE, 279 Steps: []*bbpb.Step{ 280 {Name: "Parent", EndTime: now, Status: bbpb.Status_CANCELED, 281 SummaryMarkdown: "step was never finalized; did the build crash?"}, 282 {Name: "Parent|Child", EndTime: now, Status: bbpb.Status_CANCELED, 283 SummaryMarkdown: "step was never finalized; did the build crash?"}, 284 { 285 Name: "Parent|Merge", 286 Logs: []*bbpb.Log{}, 287 MergeBuild: &bbpb.Step_MergeBuild{ 288 FromLogdogStream: "url://ns/Parent/Merge/build.proto", 289 }, 290 }, 291 }, 292 UpdateTime: now, 293 EndTime: now, 294 Output: &bbpb.Build_Output{ 295 Logs: []*bbpb.Log{{ 296 Name: "stderr", 297 Url: "url://ns/stderr", 298 ViewUrl: "view://ns/stderr", 299 }}, 300 Status: bbpb.Status_INFRA_FAILURE, 301 SummaryMarkdown: ("some stuff\n\n" + 302 "Error in build protocol: parsing Build: proto:"), 303 }, 304 }, 305 }) 306 }) 307 308 Convey(`handleNewData(nil)`, func() { 309 bs.handleNewData(nil) 310 wait() 311 bs.Drain() 312 assertStateEqual(bs.latestState, &buildState{ 313 closed: true, 314 final: true, 315 build: &bbpb.Build{ 316 SummaryMarkdown: ("some stuff\n\nError in build protocol: " + 317 "Expected a terminal build status, " + 318 "got STATUS_UNSPECIFIED, while top level status is STATUS_UNSPECIFIED."), 319 Status: bbpb.Status_INFRA_FAILURE, 320 Steps: []*bbpb.Step{ 321 {Name: "Parent", EndTime: now, Status: bbpb.Status_CANCELED, 322 SummaryMarkdown: "step was never finalized; did the build crash?"}, 323 {Name: "Parent|Child", EndTime: now, Status: bbpb.Status_CANCELED, 324 SummaryMarkdown: "step was never finalized; did the build crash?"}, 325 { 326 Name: "Parent|Merge", 327 Logs: []*bbpb.Log{}, 328 MergeBuild: &bbpb.Step_MergeBuild{ 329 FromLogdogStream: "url://ns/Parent/Merge/build.proto", 330 }, 331 }, 332 }, 333 EndTime: now, 334 UpdateTime: now, 335 Output: &bbpb.Build_Output{ 336 Logs: []*bbpb.Log{{ 337 Name: "stderr", 338 Url: "url://ns/stderr", 339 ViewUrl: "view://ns/stderr", 340 }}, 341 Status: bbpb.Status_INFRA_FAILURE, 342 SummaryMarkdown: ("some stuff\n\nError in build protocol: " + 343 "Expected a terminal build status, " + 344 "got STATUS_UNSPECIFIED, while top level status is STATUS_UNSPECIFIED."), 345 }, 346 }, 347 }) 348 }) 349 }) 350 351 Convey(`invalid build data`, func() { 352 bs.handleNewData(&logpb.LogEntry{ 353 Content: &logpb.LogEntry_Datagram{Datagram: &logpb.Datagram{ 354 Data: []byte("narpnarp"), 355 }}, 356 }) 357 wait() 358 wait() // for final build 359 bs.Drain() 360 assertStateEqual(bs.latestState, &buildState{ 361 closed: true, 362 final: true, 363 invalid: true, 364 build: &bbpb.Build{ 365 SummaryMarkdown: ("\n\nError in build protocol: parsing Build: proto:"), 366 Status: bbpb.Status_INFRA_FAILURE, 367 UpdateTime: now, 368 EndTime: now, 369 Output: &bbpb.Build_Output{ 370 Status: bbpb.Status_INFRA_FAILURE, 371 SummaryMarkdown: ("\n\nError in build protocol: parsing Build: proto:"), 372 }, 373 }, 374 }) 375 376 Convey(`ignores further updates`, func() { 377 bs.handleNewData(mkDgram(&bbpb.Build{SummaryMarkdown: "hi"})) 378 bs.Drain() 379 assertStateEqual(bs.latestState, &buildState{ 380 invalid: true, 381 final: true, 382 closed: true, 383 build: &bbpb.Build{ 384 SummaryMarkdown: ("\n\nError in build protocol: parsing Build: proto:"), 385 Status: bbpb.Status_INFRA_FAILURE, 386 UpdateTime: now, 387 EndTime: now, 388 Output: &bbpb.Build_Output{ 389 Status: bbpb.Status_INFRA_FAILURE, 390 SummaryMarkdown: ("\n\nError in build protocol: parsing Build: proto:"), 391 }, 392 }, 393 }) 394 }) 395 }) 396 397 Convey(`accept absolute url`, func() { 398 bs.handleNewData(mkDgram(&bbpb.Build{ 399 Steps: []*bbpb.Step{ 400 { 401 Name: "hi", 402 Logs: []*bbpb.Log{{ 403 Name: "foo", 404 Url: "log/foo", 405 }}, 406 }, 407 { 408 Name: "heyo", 409 Logs: []*bbpb.Log{{ 410 Name: "bar", 411 Url: "url://another_ns/log/bar", // absolute url populated 412 ViewUrl: "view://another_ns/log/bar", 413 }}, 414 }, 415 }, 416 Output: &bbpb.Build_Output{ 417 Logs: []*bbpb.Log{ 418 { 419 Name: "stderr", 420 Url: "stderr", 421 }, 422 { 423 Name: "another stderr", 424 Url: "url://another_ns/stderr", // absolute url populated 425 ViewUrl: "view://another_ns/stderr", 426 }, 427 }, 428 }, 429 })) 430 wait() 431 432 So(bs.getLatestBuild(), ShouldResembleProto, &bbpb.Build{ 433 Steps: []*bbpb.Step{ 434 { 435 Name: "hi", 436 Logs: []*bbpb.Log{{ 437 Name: "foo", 438 Url: "url://ns/log/foo", 439 ViewUrl: "view://ns/log/foo", 440 }}, 441 }, 442 { 443 Name: "heyo", 444 Logs: []*bbpb.Log{{ 445 Name: "bar", 446 Url: "url://another_ns/log/bar", 447 ViewUrl: "view://another_ns/log/bar", 448 }}, 449 }, 450 }, 451 UpdateTime: now, 452 Output: &bbpb.Build_Output{ 453 Logs: []*bbpb.Log{ 454 { 455 Name: "stderr", 456 Url: "url://ns/stderr", 457 ViewUrl: "view://ns/stderr", 458 }, 459 { 460 Name: "another stderr", 461 Url: "url://another_ns/stderr", 462 ViewUrl: "view://another_ns/stderr", 463 }, 464 }, 465 }, 466 }) 467 }) 468 469 Convey(`bad log url`, func() { 470 Convey(`step log`, func() { 471 bs.handleNewData(mkDgram(&bbpb.Build{ 472 Steps: []*bbpb.Step{ 473 { 474 Name: "hi", 475 Logs: []*bbpb.Log{{ 476 Name: "log", 477 Url: "!!badnews!!", 478 }}, 479 }, 480 }, 481 })) 482 wait() 483 wait() // for final build 484 bs.Drain() 485 assertStateEqual(bs.latestState, &buildState{ 486 closed: true, 487 final: true, 488 invalid: true, 489 build: &bbpb.Build{ 490 SummaryMarkdown: ("\n\nError in build protocol: " + 491 "step[\"hi\"].logs[\"log\"]: bad log url \"!!badnews!!\": " + 492 "segment (at 0) must begin with alphanumeric character"), 493 Steps: []*bbpb.Step{ 494 { 495 Name: "hi", 496 Status: bbpb.Status_INFRA_FAILURE, 497 EndTime: now, 498 SummaryMarkdown: ("bad log url \"!!badnews!!\": " + 499 "segment (at 0) must begin with alphanumeric character"), 500 Logs: []*bbpb.Log{{ 501 Name: "log", 502 Url: "!!badnews!!", 503 }}, 504 }, 505 }, 506 Status: bbpb.Status_INFRA_FAILURE, 507 UpdateTime: now, 508 EndTime: now, 509 Output: &bbpb.Build_Output{ 510 Status: bbpb.Status_INFRA_FAILURE, 511 SummaryMarkdown: ("\n\nError in build protocol: " + 512 "step[\"hi\"].logs[\"log\"]: bad log url \"!!badnews!!\": " + 513 "segment (at 0) must begin with alphanumeric character"), 514 }, 515 }, 516 }) 517 }) 518 519 Convey(`build log`, func() { 520 bs.handleNewData(mkDgram(&bbpb.Build{ 521 Output: &bbpb.Build_Output{ 522 Logs: []*bbpb.Log{{ 523 Name: "log", 524 Url: "!!badnews!!", 525 }}, 526 }, 527 })) 528 wait() 529 wait() // for final build 530 bs.Drain() 531 assertStateEqual(bs.latestState, &buildState{ 532 closed: true, 533 final: true, 534 invalid: true, 535 build: &bbpb.Build{ 536 SummaryMarkdown: ("\n\nError in build protocol: " + 537 "build.output.logs[\"log\"]: bad log url \"!!badnews!!\": " + 538 "segment (at 0) must begin with alphanumeric character"), 539 Output: &bbpb.Build_Output{ 540 Logs: []*bbpb.Log{{ 541 Name: "log", 542 Url: "!!badnews!!", 543 }}, 544 Status: bbpb.Status_INFRA_FAILURE, 545 SummaryMarkdown: ("\n\nError in build protocol: " + 546 "build.output.logs[\"log\"]: bad log url \"!!badnews!!\": " + 547 "segment (at 0) must begin with alphanumeric character"), 548 }, 549 Status: bbpb.Status_INFRA_FAILURE, 550 UpdateTime: now, 551 EndTime: now, 552 }, 553 }) 554 }) 555 }) 556 }) 557 }) 558 }