go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/ingestion/control/span_test.go (about) 1 // Copyright 2022 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 control 16 17 import ( 18 "context" 19 "fmt" 20 "testing" 21 "time" 22 23 "go.chromium.org/luci/server/span" 24 25 "go.chromium.org/luci/analysis/internal/testutil" 26 analysispb "go.chromium.org/luci/analysis/proto/v1" 27 28 . "github.com/smartystreets/goconvey/convey" 29 . "go.chromium.org/luci/common/testing/assertions" 30 ) 31 32 func TestSpan(t *testing.T) { 33 Convey(`With Spanner Test Database`, t, func() { 34 ctx := testutil.SpannerTestContext(t) 35 Convey(`Read`, func() { 36 entriesToCreate := []*Entry{ 37 NewEntry(0).WithBuildID("buildbucket-instance/1").Build(), 38 NewEntry(2).WithBuildID("buildbucket-instance/2").WithBuildResult(nil).Build(), 39 NewEntry(3).WithBuildID("buildbucket-instance/3").WithPresubmitResult(nil).Build(), 40 NewEntry(4).WithBuildID("buildbucket-instance/4").WithInvocationResult(nil).Build(), 41 } 42 _, err := SetEntriesForTesting(ctx, entriesToCreate...) 43 So(err, ShouldBeNil) 44 45 Convey(`None exist`, func() { 46 buildIDs := []string{"buildbucket-instance/5"} 47 results, err := Read(span.Single(ctx), buildIDs) 48 So(err, ShouldBeNil) 49 So(len(results), ShouldEqual, 1) 50 So(results[0], ShouldResembleEntry, nil) 51 }) 52 Convey(`Some exist`, func() { 53 buildIDs := []string{ 54 "buildbucket-instance/3", 55 "buildbucket-instance/4", 56 "buildbucket-instance/5", 57 "buildbucket-instance/2", 58 "buildbucket-instance/1", 59 } 60 results, err := Read(span.Single(ctx), buildIDs) 61 So(err, ShouldBeNil) 62 So(len(results), ShouldEqual, 5) 63 So(results[0], ShouldResembleEntry, entriesToCreate[2]) 64 So(results[1], ShouldResembleEntry, entriesToCreate[3]) 65 So(results[2], ShouldResembleEntry, nil) 66 So(results[3], ShouldResembleEntry, entriesToCreate[1]) 67 So(results[4], ShouldResembleEntry, entriesToCreate[0]) 68 }) 69 }) 70 Convey(`InsertOrUpdate`, func() { 71 testInsertOrUpdate := func(e *Entry) (time.Time, error) { 72 commitTime, err := span.ReadWriteTransaction(ctx, func(ctx context.Context) error { 73 return InsertOrUpdate(ctx, e) 74 }) 75 return commitTime.In(time.UTC), err 76 } 77 78 entryToCreate := NewEntry(0).Build() 79 80 _, err := SetEntriesForTesting(ctx, entryToCreate) 81 So(err, ShouldBeNil) 82 83 e := NewEntry(1).Build() 84 85 Convey(`Valid`, func() { 86 Convey(`Insert`, func() { 87 commitTime, err := testInsertOrUpdate(e) 88 So(err, ShouldBeNil) 89 e.LastUpdated = commitTime 90 91 result, err := Read(span.Single(ctx), []string{e.BuildID}) 92 So(err, ShouldBeNil) 93 So(len(result), ShouldEqual, 1) 94 So(result[0], ShouldResembleEntry, e) 95 }) 96 Convey(`Update`, func() { 97 // Update the existing entry. 98 e.BuildID = entryToCreate.BuildID 99 100 commitTime, err := testInsertOrUpdate(e) 101 So(err, ShouldBeNil) 102 e.LastUpdated = commitTime 103 104 result, err := Read(span.Single(ctx), []string{e.BuildID}) 105 So(err, ShouldBeNil) 106 So(len(result), ShouldEqual, 1) 107 So(result[0], ShouldResembleEntry, e) 108 }) 109 }) 110 Convey(`With missing Build ID`, func() { 111 e.BuildID = "" 112 _, err := testInsertOrUpdate(e) 113 So(err, ShouldErrLike, "build ID must be specified") 114 }) 115 Convey(`With invalid Build Project`, func() { 116 Convey(`Missing`, func() { 117 e.BuildProject = "" 118 _, err := testInsertOrUpdate(e) 119 So(err, ShouldErrLike, "build project: unspecified") 120 }) 121 Convey(`Invalid`, func() { 122 e.BuildProject = "!" 123 _, err := testInsertOrUpdate(e) 124 So(err, ShouldErrLike, `build project: must match ^[a-z0-9\-]{1,40}$`) 125 }) 126 }) 127 Convey(`With invalid Build Result`, func() { 128 Convey(`Missing host`, func() { 129 e.BuildResult.Host = "" 130 _, err := testInsertOrUpdate(e) 131 So(err, ShouldErrLike, "host must be specified") 132 }) 133 Convey(`Missing id`, func() { 134 e.BuildResult.Id = 0 135 _, err := testInsertOrUpdate(e) 136 So(err, ShouldErrLike, "id must be specified") 137 }) 138 Convey(`Missing creation time`, func() { 139 e.BuildResult.CreationTime = nil 140 _, err := testInsertOrUpdate(e) 141 So(err, ShouldErrLike, "build result: creation time must be specified") 142 }) 143 Convey(`Missing project`, func() { 144 e.BuildResult.Project = "" 145 _, err := testInsertOrUpdate(e) 146 So(err, ShouldErrLike, "build result: project must be specified") 147 }) 148 Convey(`Missing resultdb_host`, func() { 149 e.BuildResult.HasInvocation = true 150 e.BuildResult.ResultdbHost = "" 151 _, err := testInsertOrUpdate(e) 152 So(err, ShouldErrLike, "build result: resultdb_host must be specified if has_invocation set") 153 }) 154 Convey(`Missing builder`, func() { 155 e.BuildResult.Builder = "" 156 _, err := testInsertOrUpdate(e) 157 So(err, ShouldErrLike, "build result: builder must be specified") 158 }) 159 Convey(`Missing status`, func() { 160 e.BuildResult.Status = analysispb.BuildStatus_BUILD_STATUS_UNSPECIFIED 161 _, err := testInsertOrUpdate(e) 162 So(err, ShouldErrLike, "build result: build status must be specified") 163 }) 164 }) 165 Convey(`With invalid Invocation Project`, func() { 166 Convey(`Unspecified`, func() { 167 e.InvocationProject = "" 168 _, err := testInsertOrUpdate(e) 169 So(err, ShouldErrLike, `invocation project: unspecified`) 170 }) 171 Convey(`Invalid`, func() { 172 e.InvocationProject = "!" 173 _, err := testInsertOrUpdate(e) 174 So(err, ShouldErrLike, `invocation project: must match ^[a-z0-9\-]{1,40}$`) 175 }) 176 }) 177 Convey(`With invalid Invocation Result`, func() { 178 Convey(`Set when HasInvocation = false`, func() { 179 So(e.InvocationResult, ShouldNotBeNil) 180 e.HasInvocation = false 181 _, err := testInsertOrUpdate(e) 182 So(err, ShouldErrLike, "invocation result must not be set unless HasInvocation is set") 183 }) 184 }) 185 Convey(`With invalid Presubmit Project`, func() { 186 Convey(`Unspecified`, func() { 187 e.PresubmitProject = "" 188 _, err := testInsertOrUpdate(e) 189 So(err, ShouldErrLike, `presubmit project: unspecified`) 190 }) 191 Convey(`Invalid`, func() { 192 e.PresubmitProject = "!" 193 _, err := testInsertOrUpdate(e) 194 So(err, ShouldErrLike, `presubmit project: must match ^[a-z0-9\-]{1,40}$`) 195 }) 196 }) 197 Convey(`With invalid Presubmit Result`, func() { 198 Convey(`Set when IsPresbumit = false`, func() { 199 So(e.PresubmitResult, ShouldNotBeNil) 200 e.IsPresubmit = false 201 _, err := testInsertOrUpdate(e) 202 So(err, ShouldErrLike, "presubmit result must not be set unless IsPresubmit is set") 203 }) 204 Convey(`Missing Presubmit run ID`, func() { 205 e.PresubmitResult.PresubmitRunId = nil 206 _, err := testInsertOrUpdate(e) 207 So(err, ShouldErrLike, "presubmit run ID must be specified") 208 }) 209 Convey(`Invalid Presubmit run ID host`, func() { 210 e.PresubmitResult.PresubmitRunId.System = "!" 211 _, err := testInsertOrUpdate(e) 212 So(err, ShouldErrLike, "presubmit run system must be 'luci-cv'") 213 }) 214 Convey(`Missing Presubmit run ID system-specific ID`, func() { 215 e.PresubmitResult.PresubmitRunId.Id = "" 216 _, err := testInsertOrUpdate(e) 217 So(err, ShouldErrLike, "presubmit run system-specific ID must be specified") 218 }) 219 Convey(`Missing creation time`, func() { 220 e.PresubmitResult.CreationTime = nil 221 _, err := testInsertOrUpdate(e) 222 So(err, ShouldErrLike, "presubmit result: creation time must be specified") 223 }) 224 Convey(`Missing mode`, func() { 225 e.PresubmitResult.Mode = analysispb.PresubmitRunMode_PRESUBMIT_RUN_MODE_UNSPECIFIED 226 _, err := testInsertOrUpdate(e) 227 So(err, ShouldErrLike, "presubmit result: mode must be specified") 228 }) 229 Convey(`Missing status`, func() { 230 e.PresubmitResult.Status = analysispb.PresubmitRunStatus_PRESUBMIT_RUN_STATUS_UNSPECIFIED 231 _, err := testInsertOrUpdate(e) 232 So(err, ShouldErrLike, "presubmit result: status must be specified") 233 }) 234 }) 235 }) 236 Convey(`ReadBuildToPresubmitRunJoinStatistics`, func() { 237 Convey(`No data`, func() { 238 _, err := SetEntriesForTesting(ctx, nil...) 239 So(err, ShouldBeNil) 240 241 results, err := ReadBuildToPresubmitRunJoinStatistics(span.Single(ctx)) 242 So(err, ShouldBeNil) 243 So(results, ShouldResemble, map[string]JoinStatistics{}) 244 }) 245 Convey(`Data`, func() { 246 reference := time.Now().Add(-1 * time.Minute) 247 entriesToCreate := []*Entry{ 248 // Setup following data: 249 // Project Alpha ("alpha") := 250 // ]-1 hour, now]: 4 presubmit builds, 2 of which without 251 // presubmit result, 1 of which without 252 // build result. 253 // 1 non-presubmit build. 254 // ]-36 hours, -35 hours]: 1 presubmit build, 255 // with all results. 256 // ]-37 hours, -36 hours]: 1 presubmit build, 257 // with all results 258 // (should be ignored as >36 hours old). 259 // Project Beta ("beta") := 260 // ]-37 hours, -36 hours]: 1 presubmit build, 261 // without presubmit result. 262 NewEntry(0).WithBuildProject("alpha").WithBuildJoinedTime(reference).Build(), 263 NewEntry(1).WithBuildProject("alpha").WithBuildJoinedTime(reference).WithPresubmitResult(nil).Build(), 264 NewEntry(2).WithBuildProject("alpha").WithBuildJoinedTime(reference).WithPresubmitResult(nil).Build(), 265 NewEntry(3).WithPresubmitProject("alpha").WithPresubmitJoinedTime(reference).WithBuildResult(nil).Build(), 266 NewEntry(4).WithBuildProject("alpha").WithBuildJoinedTime(reference).WithIsPresubmit(false).WithPresubmitResult(nil).Build(), 267 NewEntry(5).WithBuildProject("alpha").WithBuildJoinedTime(reference.Add(-35 * time.Hour)).Build(), 268 NewEntry(6).WithBuildProject("alpha").WithBuildJoinedTime(reference.Add(-36 * time.Hour)).Build(), 269 NewEntry(7).WithBuildProject("beta").WithBuildJoinedTime(reference.Add(-36 * time.Hour)).WithPresubmitResult(nil).Build(), 270 } 271 _, err := SetEntriesForTesting(ctx, entriesToCreate...) 272 So(err, ShouldBeNil) 273 274 results, err := ReadBuildToPresubmitRunJoinStatistics(span.Single(ctx)) 275 So(err, ShouldBeNil) 276 277 expectedAlpha := JoinStatistics{ 278 TotalByHour: make([]int64, 36), 279 JoinedByHour: make([]int64, 36), 280 } 281 expectedAlpha.TotalByHour[0] = 3 282 expectedAlpha.JoinedByHour[0] = 1 283 expectedAlpha.TotalByHour[35] = 1 284 expectedAlpha.JoinedByHour[35] = 1 285 // Only data in the last 36 hours is included, so the build 286 // older than 36 hours is excluded. 287 288 // Expect no entry to be returned for Project beta 289 // as all data is older than 36 hours. 290 291 So(results, ShouldResemble, map[string]JoinStatistics{ 292 "alpha": expectedAlpha, 293 }) 294 }) 295 }) 296 Convey(`ReadPresubmitToBuildJoinStatistics`, func() { 297 Convey(`No data`, func() { 298 _, err := SetEntriesForTesting(ctx, nil...) 299 So(err, ShouldBeNil) 300 301 results, err := ReadPresubmitToBuildJoinStatistics(span.Single(ctx)) 302 So(err, ShouldBeNil) 303 So(results, ShouldResemble, map[string]JoinStatistics{}) 304 }) 305 Convey(`Data`, func() { 306 reference := time.Now().Add(-1 * time.Minute) 307 entriesToCreate := []*Entry{ 308 // Setup following data: 309 // Project Alpha ("alpha") := 310 // ]-1 hour, now]: 4 presubmit builds, 2 of which without 311 // build result, 1 of which without 312 // presubmit result. 313 // 1 non-presubmit build. 314 // ]-36 hours, -35 hours]: 1 presubmit build, 315 // with all results. 316 // ]-37 hours, -36 hours]: 1 presubmit build, 317 // with all results 318 // (should be ignored as >36 hours old). 319 // Project Beta ("beta") := 320 // ]-37 hours, -36 hours]: 1 presubmit build, 321 // without build result. 322 NewEntry(0).WithPresubmitProject("alpha").WithPresubmitJoinedTime(reference).Build(), 323 NewEntry(1).WithPresubmitProject("alpha").WithPresubmitJoinedTime(reference).WithBuildResult(nil).Build(), 324 NewEntry(2).WithPresubmitProject("alpha").WithPresubmitJoinedTime(reference).WithBuildResult(nil).Build(), 325 NewEntry(3).WithBuildProject("alpha").WithBuildJoinedTime(reference).WithPresubmitResult(nil).Build(), 326 NewEntry(4).WithPresubmitProject("alpha").WithPresubmitJoinedTime(reference).WithIsPresubmit(false).WithBuildResult(nil).Build(), 327 NewEntry(5).WithPresubmitProject("alpha").WithPresubmitJoinedTime(reference.Add(-35 * time.Hour)).Build(), 328 NewEntry(6).WithPresubmitProject("alpha").WithPresubmitJoinedTime(reference.Add(-36 * time.Hour)).Build(), 329 NewEntry(7).WithPresubmitProject("beta").WithPresubmitJoinedTime(reference.Add(-36 * time.Hour)).WithBuildResult(nil).Build(), 330 } 331 _, err := SetEntriesForTesting(ctx, entriesToCreate...) 332 So(err, ShouldBeNil) 333 334 results, err := ReadPresubmitToBuildJoinStatistics(span.Single(ctx)) 335 So(err, ShouldBeNil) 336 337 expectedAlpha := JoinStatistics{ 338 TotalByHour: make([]int64, 36), 339 JoinedByHour: make([]int64, 36), 340 } 341 expectedAlpha.TotalByHour[0] = 3 342 expectedAlpha.JoinedByHour[0] = 1 343 expectedAlpha.TotalByHour[35] = 1 344 expectedAlpha.JoinedByHour[35] = 1 345 // Only data in the last 36 hours is included, so the build 346 // older than 36 hours is excluded. 347 348 // Expect no entry to be returned for Project beta 349 // as all data is older than 36 hours. 350 351 So(results, ShouldResemble, map[string]JoinStatistics{ 352 "alpha": expectedAlpha, 353 }) 354 }) 355 }) 356 Convey(`ReadBuildToInvocationJoinStatistics`, func() { 357 Convey(`No data`, func() { 358 _, err := SetEntriesForTesting(ctx, nil...) 359 So(err, ShouldBeNil) 360 361 results, err := ReadBuildToInvocationJoinStatistics(span.Single(ctx)) 362 So(err, ShouldBeNil) 363 So(results, ShouldResemble, map[string]JoinStatistics{}) 364 }) 365 Convey(`Data`, func() { 366 reference := time.Now().Add(-1 * time.Minute) 367 entriesToCreate := []*Entry{ 368 // Setup following data: 369 // Project Alpha ("alpha") := 370 // ]-1 hour, now]: 4 builds /w invocation, 2 of which without 371 // invocation result, 1 of which without 372 // build result. 373 // 1 build w/o invocation. 374 // ]-36 hours, -35 hours]: 1 build /w invocation, 375 // with all results. 376 // ]-37 hours, -36 hours]: 1 build /w invocation, 377 // with all results 378 // (should be ignored as >36 hours old). 379 // Project Beta ("beta") := 380 // ]-37 hours, -36 hours]: 1 build /w invocation, 381 // without invocation result. 382 NewEntry(0).WithBuildProject("alpha").WithBuildJoinedTime(reference).Build(), 383 NewEntry(1).WithBuildProject("alpha").WithBuildJoinedTime(reference).WithInvocationResult(nil).Build(), 384 NewEntry(2).WithBuildProject("alpha").WithBuildJoinedTime(reference).WithInvocationResult(nil).Build(), 385 NewEntry(3).WithInvocationProject("alpha").WithInvocationJoinedTime(reference).WithBuildResult(nil).Build(), 386 NewEntry(4).WithBuildProject("alpha").WithBuildJoinedTime(reference).WithHasInvocation(false).WithInvocationResult(nil).Build(), 387 NewEntry(5).WithBuildProject("alpha").WithBuildJoinedTime(reference.Add(-35 * time.Hour)).Build(), 388 NewEntry(6).WithBuildProject("alpha").WithBuildJoinedTime(reference.Add(-36 * time.Hour)).Build(), 389 NewEntry(7).WithBuildProject("beta").WithBuildJoinedTime(reference.Add(-36 * time.Hour)).WithInvocationResult(nil).Build(), 390 } 391 _, err := SetEntriesForTesting(ctx, entriesToCreate...) 392 So(err, ShouldBeNil) 393 394 results, err := ReadBuildToInvocationJoinStatistics(span.Single(ctx)) 395 So(err, ShouldBeNil) 396 397 expectedAlpha := JoinStatistics{ 398 TotalByHour: make([]int64, 36), 399 JoinedByHour: make([]int64, 36), 400 } 401 expectedAlpha.TotalByHour[0] = 3 402 expectedAlpha.JoinedByHour[0] = 1 403 expectedAlpha.TotalByHour[35] = 1 404 expectedAlpha.JoinedByHour[35] = 1 405 // Only data in the last 36 hours is included, so the build 406 // older than 36 hours is excluded. 407 408 // Expect no entry to be returned for Project beta 409 // as all data is older than 36 hours. 410 411 So(results, ShouldResemble, map[string]JoinStatistics{ 412 "alpha": expectedAlpha, 413 }) 414 }) 415 }) 416 Convey(`ReadInvocationToBuildJoinStatistics`, func() { 417 Convey(`No data`, func() { 418 _, err := SetEntriesForTesting(ctx, nil...) 419 So(err, ShouldBeNil) 420 421 results, err := ReadInvocationToBuildJoinStatistics(span.Single(ctx)) 422 So(err, ShouldBeNil) 423 So(results, ShouldResemble, map[string]JoinStatistics{}) 424 }) 425 Convey(`Data`, func() { 426 reference := time.Now().Add(-1 * time.Minute) 427 entriesToCreate := []*Entry{ 428 // Setup following data: 429 // Project Alpha ("alpha") := 430 // ]-1 hour, now]: 4 builds /w invocation, 2 of which without 431 // build result, 1 of which without 432 // invocation result. 433 // 1 build w/o invocation. 434 // ]-36 hours, -35 hours]: 1 build /w invocation, 435 // with all results. 436 // ]-37 hours, -36 hours]: 1 build /w invocation, 437 // with all results 438 // (should be ignored as >36 hours old). 439 // Project Beta ("beta") := 440 // ]-37 hours, -36 hours]: 1 build /w invocation, 441 // without build result. 442 NewEntry(0).WithInvocationProject("alpha").WithInvocationJoinedTime(reference).Build(), 443 NewEntry(1).WithInvocationProject("alpha").WithInvocationJoinedTime(reference).WithBuildResult(nil).Build(), 444 NewEntry(2).WithInvocationProject("alpha").WithInvocationJoinedTime(reference).WithBuildResult(nil).Build(), 445 NewEntry(3).WithBuildProject("alpha").WithBuildJoinedTime(reference).WithInvocationResult(nil).Build(), 446 NewEntry(4).WithInvocationProject("alpha").WithInvocationJoinedTime(reference).WithHasInvocation(false).WithBuildResult(nil).Build(), 447 NewEntry(5).WithInvocationProject("alpha").WithInvocationJoinedTime(reference.Add(-35 * time.Hour)).Build(), 448 NewEntry(6).WithInvocationProject("alpha").WithInvocationJoinedTime(reference.Add(-36 * time.Hour)).Build(), 449 NewEntry(7).WithInvocationProject("beta").WithInvocationJoinedTime(reference.Add(-36 * time.Hour)).WithBuildResult(nil).Build(), 450 } 451 _, err := SetEntriesForTesting(ctx, entriesToCreate...) 452 So(err, ShouldBeNil) 453 454 results, err := ReadInvocationToBuildJoinStatistics(span.Single(ctx)) 455 So(err, ShouldBeNil) 456 457 expectedAlpha := JoinStatistics{ 458 TotalByHour: make([]int64, 36), 459 JoinedByHour: make([]int64, 36), 460 } 461 expectedAlpha.TotalByHour[0] = 3 462 expectedAlpha.JoinedByHour[0] = 1 463 expectedAlpha.TotalByHour[35] = 1 464 expectedAlpha.JoinedByHour[35] = 1 465 // Only data in the last 36 hours is included, so the build 466 // older than 36 hours is excluded. 467 468 // Expect no entry to be returned for Project beta 469 // as all data is older than 36 hours. 470 471 So(results, ShouldResemble, map[string]JoinStatistics{ 472 "alpha": expectedAlpha, 473 }) 474 }) 475 }) 476 }) 477 } 478 479 func ShouldResembleEntry(actual any, expected ...any) string { 480 if len(expected) != 1 { 481 return fmt.Sprintf("ShouldResembleEntry expects 1 value, got %d", len(expected)) 482 } 483 exp := expected[0] 484 if exp == nil { 485 return ShouldBeNil(actual) 486 } 487 488 a, ok := actual.(*Entry) 489 if !ok { 490 return "actual should be of type *Entry" 491 } 492 e, ok := exp.(*Entry) 493 if !ok { 494 return "expected value should be of type *Entry" 495 } 496 497 // Check equality of non-proto fields. 498 a.BuildResult = nil 499 a.PresubmitResult = nil 500 e.BuildResult = nil 501 e.PresubmitResult = nil 502 if msg := ShouldResemble(a, e); msg != "" { 503 return msg 504 } 505 506 // Check equality of proto fields. 507 if msg := ShouldResembleProto(a.BuildResult, e.BuildResult); msg != "" { 508 return msg 509 } 510 if msg := ShouldResembleProto(a.PresubmitResult, e.PresubmitResult); msg != "" { 511 return msg 512 } 513 return "" 514 }