go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/testresults/query_test.go (about) 1 // Copyright 2020 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 testresults 16 17 import ( 18 "context" 19 "sort" 20 "strings" 21 "testing" 22 "time" 23 24 . "github.com/smartystreets/goconvey/convey" 25 "google.golang.org/grpc/codes" 26 durpb "google.golang.org/protobuf/types/known/durationpb" 27 "google.golang.org/protobuf/types/known/structpb" 28 "google.golang.org/protobuf/types/known/timestamppb" 29 30 "go.chromium.org/luci/common/clock/testclock" 31 "go.chromium.org/luci/common/proto/mask" 32 . "go.chromium.org/luci/common/testing/assertions" 33 "go.chromium.org/luci/resultdb/internal/invocations" 34 "go.chromium.org/luci/resultdb/internal/testutil" 35 "go.chromium.org/luci/resultdb/internal/testutil/insert" 36 "go.chromium.org/luci/resultdb/pbutil" 37 pb "go.chromium.org/luci/resultdb/proto/v1" 38 "go.chromium.org/luci/server/span" 39 ) 40 41 func TestQueryTestResults(t *testing.T) { 42 Convey(`QueryTestResults`, t, func() { 43 ctx := testutil.SpannerTestContext(t) 44 45 testutil.MustApply(ctx, insert.Invocation("inv1", pb.Invocation_ACTIVE, map[string]any{ 46 "CommonTestIDPrefix": "", 47 "TestResultVariantUnion": []string{"a:b", "k:1", "k:2", "k2:1"}, 48 })) 49 50 q := &Query{ 51 Predicate: &pb.TestResultPredicate{}, 52 PageSize: 100, 53 InvocationIDs: invocations.NewIDSet("inv1"), 54 Mask: AllFields, 55 } 56 57 fetch := func(q *Query) (trs []*pb.TestResult, token string, err error) { 58 ctx, cancel := span.ReadOnlyTransaction(ctx) 59 defer cancel() 60 return q.Fetch(ctx) 61 } 62 63 mustFetch := func(q *Query) (trs []*pb.TestResult, token string) { 64 trs, token, err := fetch(q) 65 So(err, ShouldBeNil) 66 return 67 } 68 69 mustFetchNames := func(q *Query) []string { 70 trs, _, err := fetch(q) 71 So(err, ShouldBeNil) 72 names := make([]string, len(trs)) 73 for i, a := range trs { 74 names[i] = a.Name 75 } 76 sort.Strings(names) 77 return names 78 } 79 80 Convey(`Does not fetch test results of other invocations`, func() { 81 expected := insert.MakeTestResults("inv1", "DoBaz", nil, 82 pb.TestStatus_PASS, 83 pb.TestStatus_FAIL, 84 pb.TestStatus_FAIL, 85 pb.TestStatus_PASS, 86 pb.TestStatus_FAIL, 87 ) 88 testutil.MustApply(ctx, 89 insert.Invocation("inv0", pb.Invocation_ACTIVE, nil), 90 insert.Invocation("inv2", pb.Invocation_ACTIVE, nil), 91 ) 92 testutil.MustApply(ctx, testutil.CombineMutations( 93 insert.TestResults("inv0", "X", nil, pb.TestStatus_PASS, pb.TestStatus_FAIL), 94 insert.TestResultMessages(expected), 95 insert.TestResults("inv2", "Y", nil, pb.TestStatus_PASS, pb.TestStatus_FAIL), 96 )...) 97 98 actual, _ := mustFetch(q) 99 So(actual, ShouldResembleProto, expected) 100 }) 101 102 Convey(`Expectancy filter`, func() { 103 testutil.MustApply(ctx, insert.Invocation("inv0", pb.Invocation_ACTIVE, map[string]any{ 104 "CommonTestIDPrefix": "", 105 "TestResultVariantUnion": pbutil.Variant("a", "b"), 106 })) 107 q.InvocationIDs = invocations.NewIDSet("inv0", "inv1") 108 109 Convey(`VARIANTS_WITH_UNEXPECTED_RESULTS`, func() { 110 q.Predicate.Expectancy = pb.TestResultPredicate_VARIANTS_WITH_UNEXPECTED_RESULTS 111 112 testutil.MustApply(ctx, testutil.CombineMutations( 113 insert.TestResults("inv0", "T1", nil, pb.TestStatus_PASS, pb.TestStatus_FAIL), 114 insert.TestResults("inv0", "T2", nil, pb.TestStatus_PASS), 115 insert.TestResults("inv1", "T1", nil, pb.TestStatus_PASS), 116 insert.TestResults("inv1", "T2", nil, pb.TestStatus_FAIL), 117 insert.TestResults("inv1", "T3", nil, pb.TestStatus_PASS), 118 insert.TestResults("inv1", "T4", pbutil.Variant("a", "b"), pb.TestStatus_FAIL), 119 insert.TestExonerations("inv0", "T1", nil, pb.ExonerationReason_OCCURS_ON_OTHER_CLS), 120 )...) 121 122 Convey(`Works`, func() { 123 So(mustFetchNames(q), ShouldResemble, []string{ 124 "invocations/inv0/tests/T1/results/0", 125 "invocations/inv0/tests/T1/results/1", 126 "invocations/inv0/tests/T2/results/0", 127 "invocations/inv1/tests/T1/results/0", 128 "invocations/inv1/tests/T2/results/0", 129 "invocations/inv1/tests/T4/results/0", 130 }) 131 }) 132 133 Convey(`TestID filter`, func() { 134 q.Predicate.TestIdRegexp = ".*T4" 135 So(mustFetchNames(q), ShouldResemble, []string{ 136 "invocations/inv1/tests/T4/results/0", 137 }) 138 }) 139 140 Convey(`Variant filter`, func() { 141 q.Predicate.Variant = &pb.VariantPredicate{ 142 Predicate: &pb.VariantPredicate_Equals{ 143 Equals: pbutil.Variant("a", "b"), 144 }, 145 } 146 So(mustFetchNames(q), ShouldResemble, []string{ 147 "invocations/inv1/tests/T4/results/0", 148 }) 149 }) 150 151 Convey(`ExcludeExonerated`, func() { 152 q.Predicate.ExcludeExonerated = true 153 So(mustFetchNames(q), ShouldResemble, []string{ 154 "invocations/inv0/tests/T2/results/0", 155 "invocations/inv1/tests/T2/results/0", 156 "invocations/inv1/tests/T4/results/0", 157 }) 158 }) 159 }) 160 161 Convey(`VARIANTS_WITH_ONLY_UNEXPECTED_RESULTS`, func() { 162 q.Predicate.Expectancy = pb.TestResultPredicate_VARIANTS_WITH_ONLY_UNEXPECTED_RESULTS 163 164 testutil.MustApply(ctx, testutil.CombineMutations( 165 insert.TestResults("inv0", "flaky", nil, pb.TestStatus_PASS, pb.TestStatus_FAIL), 166 insert.TestResults("inv0", "passing", nil, pb.TestStatus_PASS), 167 insert.TestResults("inv0", "F0", pbutil.Variant("a", "0"), pb.TestStatus_FAIL), 168 insert.TestResults("inv0", "in_both_invocations", nil, pb.TestStatus_FAIL), 169 // Same test, but different variant. 170 insert.TestResults("inv1", "F0", pbutil.Variant("a", "1"), pb.TestStatus_PASS), 171 insert.TestResults("inv1", "in_both_invocations", nil, pb.TestStatus_PASS), 172 insert.TestResults("inv1", "F1", nil, pb.TestStatus_FAIL, pb.TestStatus_FAIL), 173 )...) 174 175 Convey(`Works`, func() { 176 So(mustFetchNames(q), ShouldResemble, []string{ 177 "invocations/inv0/tests/F0/results/0", 178 "invocations/inv1/tests/F1/results/0", 179 "invocations/inv1/tests/F1/results/1", 180 }) 181 }) 182 }) 183 }) 184 185 Convey(`Test id filter`, func() { 186 testutil.MustApply(ctx, 187 insert.Invocation("inv0", pb.Invocation_ACTIVE, map[string]any{ 188 "CommonTestIDPrefix": "1-", 189 }), 190 insert.Invocation("inv2", pb.Invocation_ACTIVE, map[string]any{ 191 "CreateTime": time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), 192 }), 193 ) 194 testutil.MustApply(ctx, testutil.CombineMutations( 195 insert.TestResults("inv0", "1-1", nil, pb.TestStatus_PASS, pb.TestStatus_FAIL), 196 insert.TestResults("inv0", "1-2", nil, pb.TestStatus_PASS), 197 insert.TestResults("inv1", "1-1", nil, pb.TestStatus_PASS), 198 insert.TestResults("inv1", "2-1", nil, pb.TestStatus_PASS), 199 insert.TestResults("inv1", "2", nil, pb.TestStatus_FAIL), 200 insert.TestResults("inv2", "1-2", nil, pb.TestStatus_PASS), 201 )...) 202 203 q.InvocationIDs = invocations.NewIDSet("inv0", "inv1", "inv2") 204 q.Predicate.TestIdRegexp = "1-.+" 205 206 So(mustFetchNames(q), ShouldResemble, []string{ 207 "invocations/inv0/tests/1-1/results/0", 208 "invocations/inv0/tests/1-1/results/1", 209 "invocations/inv0/tests/1-2/results/0", 210 "invocations/inv1/tests/1-1/results/0", 211 "invocations/inv2/tests/1-2/results/0", 212 }) 213 }) 214 215 Convey(`Variant equals`, func() { 216 v1 := pbutil.Variant("k", "1") 217 v2 := pbutil.Variant("k", "2") 218 testutil.MustApply(ctx, 219 insert.Invocation("inv0", pb.Invocation_ACTIVE, map[string]any{ 220 "TestResultVariantUnion": []string{"k:1", "k:2"}, 221 }), 222 insert.Invocation("inv2", pb.Invocation_ACTIVE, map[string]any{ 223 "CreateTime": time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), 224 }), 225 ) 226 testutil.MustApply(ctx, testutil.CombineMutations( 227 insert.TestResults("inv0", "1-1", v1, pb.TestStatus_PASS, pb.TestStatus_FAIL), 228 insert.TestResults("inv0", "1-2", v2, pb.TestStatus_PASS), 229 insert.TestResults("inv1", "1-1", v1, pb.TestStatus_PASS), 230 insert.TestResults("inv1", "2-1", v2, pb.TestStatus_PASS), 231 insert.TestResults("inv2", "1-1", v1, pb.TestStatus_PASS), 232 )...) 233 234 q.InvocationIDs = invocations.NewIDSet("inv0", "inv1", "inv2") 235 q.Predicate.Variant = &pb.VariantPredicate{ 236 Predicate: &pb.VariantPredicate_Equals{Equals: v1}, 237 } 238 239 So(mustFetchNames(q), ShouldResemble, []string{ 240 "invocations/inv0/tests/1-1/results/0", 241 "invocations/inv0/tests/1-1/results/1", 242 "invocations/inv1/tests/1-1/results/0", 243 "invocations/inv2/tests/1-1/results/0", 244 }) 245 }) 246 247 Convey(`Variant contains`, func() { 248 testutil.MustApply(ctx, insert.Invocation("inv0", pb.Invocation_ACTIVE, map[string]any{ 249 "TestResultVariantUnion": []string{"k:1", "k:2", "k2:1"}, 250 })) 251 252 v1 := pbutil.Variant("k", "1") 253 v11 := pbutil.Variant("k", "1", "k2", "1") 254 v2 := pbutil.Variant("k", "2") 255 testutil.MustApply(ctx, testutil.CombineMutations( 256 insert.TestResults("inv0", "1-1", v1, pb.TestStatus_PASS, pb.TestStatus_FAIL), 257 insert.TestResults("inv0", "1-2", v11, pb.TestStatus_PASS), 258 insert.TestResults("inv1", "1-1", v1, pb.TestStatus_PASS), 259 insert.TestResults("inv1", "2-1", v2, pb.TestStatus_PASS), 260 )...) 261 262 q.InvocationIDs = invocations.NewIDSet("inv0", "inv1") 263 264 Convey(`Empty`, func() { 265 q.Predicate.Variant = &pb.VariantPredicate{ 266 Predicate: &pb.VariantPredicate_Contains{Contains: pbutil.Variant()}, 267 } 268 269 So(mustFetchNames(q), ShouldResemble, []string{ 270 "invocations/inv0/tests/1-1/results/0", 271 "invocations/inv0/tests/1-1/results/1", 272 "invocations/inv0/tests/1-2/results/0", 273 "invocations/inv1/tests/1-1/results/0", 274 "invocations/inv1/tests/2-1/results/0", 275 }) 276 }) 277 278 Convey(`Non-empty`, func() { 279 q.Predicate.Variant = &pb.VariantPredicate{ 280 Predicate: &pb.VariantPredicate_Contains{Contains: v1}, 281 } 282 283 So(mustFetchNames(q), ShouldResemble, []string{ 284 "invocations/inv0/tests/1-1/results/0", 285 "invocations/inv0/tests/1-1/results/1", 286 "invocations/inv0/tests/1-2/results/0", 287 "invocations/inv1/tests/1-1/results/0", 288 }) 289 }) 290 }) 291 292 Convey(`Paging`, func() { 293 trs := insert.MakeTestResults("inv1", "DoBaz", nil, 294 pb.TestStatus_PASS, 295 pb.TestStatus_FAIL, 296 pb.TestStatus_FAIL, 297 pb.TestStatus_PASS, 298 pb.TestStatus_FAIL, 299 ) 300 testutil.MustApply(ctx, insert.TestResultMessages(trs)...) 301 302 mustReadPage := func(pageToken string, pageSize int, expected []*pb.TestResult) string { 303 q2 := q 304 q2.PageToken = pageToken 305 q2.PageSize = pageSize 306 actual, token := mustFetch(q2) 307 So(actual, ShouldResembleProto, expected) 308 return token 309 } 310 311 Convey(`All results`, func() { 312 token := mustReadPage("", 10, trs) 313 So(token, ShouldEqual, "") 314 }) 315 316 Convey(`With pagination`, func() { 317 token := mustReadPage("", 1, trs[:1]) 318 So(token, ShouldNotEqual, "") 319 320 token = mustReadPage(token, 4, trs[1:]) 321 So(token, ShouldNotEqual, "") 322 323 token = mustReadPage(token, 5, nil) 324 So(token, ShouldEqual, "") 325 }) 326 327 Convey(`Bad token`, func() { 328 ctx, cancel := span.ReadOnlyTransaction(ctx) 329 defer cancel() 330 331 Convey(`From bad position`, func() { 332 q.PageToken = "CgVoZWxsbw==" 333 _, _, err := q.Fetch(ctx) 334 So(err, ShouldHaveAppStatus, codes.InvalidArgument, "invalid page_token") 335 }) 336 337 Convey(`From decoding`, func() { 338 q.PageToken = "%%%" 339 _, _, err := q.Fetch(ctx) 340 So(err, ShouldHaveAppStatus, codes.InvalidArgument, "invalid page_token") 341 }) 342 }) 343 }) 344 345 Convey(`Test metadata`, func() { 346 expected := insert.MakeTestResults("inv1", "DoBaz", nil, pb.TestStatus_PASS) 347 expected[0].TestMetadata = &pb.TestMetadata{ 348 Name: "original_name", 349 Location: &pb.TestLocation{ 350 FileName: "//a_test.go", 351 Line: 54, 352 }, 353 BugComponent: &pb.BugComponent{ 354 System: &pb.BugComponent_Monorail{ 355 Monorail: &pb.MonorailComponent{ 356 Project: "chromium", 357 Value: "Component>Value", 358 }, 359 }, 360 }, 361 } 362 testutil.MustApply(ctx, insert.Invocation("inv", pb.Invocation_ACTIVE, nil)) 363 testutil.MustApply(ctx, insert.TestResultMessages(expected)...) 364 365 actual, _ := mustFetch(q) 366 So(actual, ShouldResembleProto, expected) 367 }) 368 369 Convey(`Failure reason`, func() { 370 expected := insert.MakeTestResults("inv1", "DoFailureReason", nil, pb.TestStatus_PASS) 371 expected[0].FailureReason = &pb.FailureReason{ 372 PrimaryErrorMessage: "want true, got false", 373 Errors: []*pb.FailureReason_Error{ 374 {Message: "want true, got false"}, 375 {Message: "want false, got true"}, 376 }, 377 TruncatedErrorsCount: 0, 378 } 379 testutil.MustApply(ctx, insert.Invocation("inv", pb.Invocation_ACTIVE, nil)) 380 testutil.MustApply(ctx, insert.TestResultMessages(expected)...) 381 382 actual, _ := mustFetch(q) 383 So(actual, ShouldResembleProto, expected) 384 }) 385 386 Convey(`Properties`, func() { 387 expected := insert.MakeTestResults("inv1", "WithProperties", nil, pb.TestStatus_PASS) 388 expected[0].Properties = &structpb.Struct{Fields: map[string]*structpb.Value{ 389 "key": structpb.NewStringValue("value"), 390 }} 391 testutil.MustApply(ctx, insert.Invocation("inv", pb.Invocation_ACTIVE, nil)) 392 testutil.MustApply(ctx, insert.TestResultMessages(expected)...) 393 394 actual, _ := mustFetch(q) 395 So(actual, ShouldResembleProto, expected) 396 }) 397 398 Convey(`Skip reason`, func() { 399 expected := insert.MakeTestResults("inv1", "WithSkipReason", nil, pb.TestStatus_SKIP) 400 expected[0].SkipReason = pb.SkipReason_AUTOMATICALLY_DISABLED_FOR_FLAKINESS 401 testutil.MustApply(ctx, insert.Invocation("inv", pb.Invocation_ACTIVE, nil)) 402 testutil.MustApply(ctx, insert.TestResultMessages(expected)...) 403 404 actual, _ := mustFetch(q) 405 So(actual, ShouldResembleProto, expected) 406 }) 407 408 Convey(`Variant in the mask`, func() { 409 testutil.MustApply(ctx, insert.Invocation("inv0", pb.Invocation_ACTIVE, nil)) 410 411 v1 := pbutil.Variant("k", "1") 412 v2 := pbutil.Variant("k", "2") 413 testutil.MustApply(ctx, testutil.CombineMutations( 414 insert.TestResults("inv0", "1-1", v1, pb.TestStatus_PASS, pb.TestStatus_FAIL), 415 insert.TestResults("inv0", "1-2", v2, pb.TestStatus_PASS), 416 insert.TestResults("inv1", "1-1", v1, pb.TestStatus_PASS), 417 insert.TestResults("inv1", "2-1", v2, pb.TestStatus_PASS), 418 )...) 419 420 q.InvocationIDs = invocations.NewIDSet("inv0", "inv1") 421 422 Convey(`Present`, func() { 423 q.Mask = mask.MustFromReadMask(&pb.TestResult{}, "name", "test_id", "variant", "variant_hash") 424 results, _ := mustFetch(q) 425 for _, r := range results { 426 So(r.Variant, ShouldNotBeNil) 427 So(r.VariantHash, ShouldNotEqual, "") 428 } 429 }) 430 431 Convey(`Not present`, func() { 432 q.Mask = mask.MustFromReadMask(&pb.TestResult{}, "name", "test_id") 433 results, _ := mustFetch(q) 434 for _, r := range results { 435 So(r.Variant, ShouldBeNil) 436 So(r.VariantHash, ShouldEqual, "") 437 } 438 }) 439 }) 440 441 Convey(`Empty list of invocations`, func() { 442 q.InvocationIDs = invocations.NewIDSet() 443 actual, _ := mustFetch(q) 444 So(actual, ShouldHaveLength, 0) 445 }) 446 447 Convey(`Filter invocations`, func() { 448 testutil.MustApply(ctx, 449 insert.Invocation("inv0", pb.Invocation_ACTIVE, map[string]any{ 450 "CommonTestIDPrefix": "ninja://browser_tests/", 451 }), 452 insert.Invocation("inv2", pb.Invocation_ACTIVE, nil), 453 ) 454 455 v1 := pbutil.Variant("k", "1") 456 v2 := pbutil.Variant("k", "2") 457 testutil.MustApply(ctx, testutil.CombineMutations( 458 insert.TestResults("inv0", "ninja://browser_tests/1-1", v1, pb.TestStatus_PASS, pb.TestStatus_FAIL), 459 insert.TestResults("inv0", "1-2", v2, pb.TestStatus_PASS), 460 insert.TestResults("inv1", "ninja://browser_tests/1-1", v1, pb.TestStatus_PASS), 461 insert.TestResults("inv1", "2-1", v2, pb.TestStatus_PASS), 462 insert.TestResults("inv2", "ninja://browser_tests/1-1", v1, pb.TestStatus_PASS), 463 )...) 464 465 q.InvocationIDs = invocations.NewIDSet("inv0", "inv1") 466 q.Predicate.TestIdRegexp = "ninja://browser_tests/.*" 467 468 results, _ := mustFetch(q) 469 for _, r := range results { 470 So(r.Name, ShouldNotStartWith, "invocations/inv2") 471 So(r.TestId, ShouldEqual, "ninja://browser_tests/1-1") 472 } 473 }) 474 475 Convey(`Query statements`, func() { 476 Convey(`only unexpected exclude exonerated`, func() { 477 st := q.genStatement("testResults", map[string]any{ 478 "params": map[string]any{ 479 "invIDs": q.InvocationIDs, 480 "afterInvocationId": "build-123", 481 "afterTestId": "test", 482 "afterResultId": "result", 483 }, 484 "columns": "InvocationId, TestId, VariantHash", 485 "onlyUnexpected": true, 486 "withUnexpected": true, 487 "excludeExonerated": true, 488 }) 489 expected := ` 490 @{USE_ADDITIONAL_PARALLELISM=TRUE} 491 WITH 492 invs AS ( 493 SELECT * 494 FROM UNNEST(@invIDs) 495 AS InvocationId 496 ), 497 testVariants AS ( 498 SELECT DISTINCT TestId, VariantHash 499 FROM invs 500 JOIN TestResults@{FORCE_INDEX=UnexpectedTestResults, spanner_emulator.disable_query_null_filtered_index_check=true} USING(InvocationId) 501 WHERE IsUnexpected 502 ), 503 exonerated AS ( 504 SELECT DISTINCT TestId, VariantHash 505 FROM TestExonerations 506 WHERE InvocationId IN UNNEST(@invIDs) 507 ), 508 variantsWithUnexpectedResults AS ( 509 SELECT 510 tv.* 511 FROM testVariants tv 512 LEFT JOIN exonerated USING(TestId, VariantHash) 513 WHERE exonerated.TestId IS NULL 514 ), 515 withUnexpected AS ( 516 SELECT InvocationId, TestId, VariantHash 517 FROM invs 518 JOIN ( 519 variantsWithUnexpectedResults vur 520 JOIN@{FORCE_JOIN_ORDER=TRUE, JOIN_METHOD=HASH_JOIN} TestResults tr USING (TestId, VariantHash) 521 ) USING(InvocationId) 522 ) , 523 withOnlyUnexpected AS ( 524 SELECT ARRAY_AGG(tr) trs 525 FROM withUnexpected tr 526 GROUP BY TestId, VariantHash 527 HAVING LOGICAL_AND(IFNULL(IsUnexpected, false)) 528 ) 529 SELECT tr.* 530 FROM withOnlyUnexpected owu, owu.trs tr 531 WHERE true 532 AND ( 533 (InvocationId > @afterInvocationId) OR 534 (InvocationId = @afterInvocationId AND TestId > @afterTestId) OR 535 (InvocationId = @afterInvocationId AND TestId = @afterTestId AND ResultId > @afterResultId) 536 ) 537 ORDER BY InvocationId, TestId, ResultId 538 ` 539 So(strings.Join(strings.Fields(st.SQL), " "), ShouldEqual, strings.Join(strings.Fields(expected), " ")) 540 }) 541 542 Convey(`with unexpected filter by testID`, func() { 543 st := q.genStatement("testResults", map[string]any{ 544 "params": map[string]any{ 545 "invIDs": q.InvocationIDs, 546 "testIdRegexp": "^T4$", 547 "afterInvocationId": "build-123", 548 "afterTestId": "test", 549 "afterResultId": "result", 550 }, 551 "columns": "InvocationId, TestId, VariantHash", 552 "onlyUnexpected": false, 553 "withUnexpected": true, 554 "excludeExonerated": false, 555 }) 556 expected := ` 557 @{USE_ADDITIONAL_PARALLELISM=TRUE} 558 WITH 559 invs AS ( 560 SELECT * 561 FROM UNNEST(@invIDs) 562 AS InvocationId 563 ), 564 variantsWithUnexpectedResults AS ( 565 SELECT DISTINCT TestId, VariantHash 566 FROM invs 567 JOIN TestResults@{FORCE_INDEX=UnexpectedTestResults, spanner_emulator.disable_query_null_filtered_index_check=true} USING(InvocationId) 568 WHERE IsUnexpected 569 AND REGEXP_CONTAINS(TestId, @testIdRegexp) 570 ), 571 withUnexpected AS ( 572 SELECT InvocationId, TestId, VariantHash 573 FROM invs 574 JOIN ( 575 variantsWithUnexpectedResults vur 576 JOIN@{FORCE_JOIN_ORDER=TRUE, JOIN_METHOD=HASH_JOIN} TestResults tr USING (TestId, VariantHash) 577 ) USING(InvocationId) 578 ) 579 SELECT * FROM withUnexpected 580 WHERE true 581 AND ( 582 (InvocationId > @afterInvocationId) OR 583 (InvocationId = @afterInvocationId AND TestId > @afterTestId) OR 584 (InvocationId = @afterInvocationId AND TestId = @afterTestId AND ResultId > @afterResultId) 585 ) 586 ORDER BY InvocationId, TestId, ResultId 587 ` 588 // Compare sql strings ignoring whitespaces. 589 So(strings.Join(strings.Fields(st.SQL), " "), ShouldEqual, strings.Join(strings.Fields(expected), " ")) 590 }) 591 }) 592 }) 593 } 594 595 func TestToLimitedData(t *testing.T) { 596 ctx := context.Background() 597 598 Convey(`ToLimitedData`, t, func() { 599 invID := "inv0" 600 testID := "FooBar" 601 resultID := "123" 602 name := pbutil.TestResultName(invID, testID, resultID) 603 variant := pbutil.Variant() 604 variantHash := pbutil.VariantHash(variant) 605 606 Convey(`masks fields`, func() { 607 testResult := &pb.TestResult{ 608 Name: name, 609 TestId: testID, 610 ResultId: resultID, 611 Variant: variant, 612 Expected: true, 613 Status: pb.TestStatus_PASS, 614 SummaryHtml: "SummaryHtml", 615 StartTime: ×tamppb.Timestamp{Seconds: 1000, Nanos: 1234}, 616 Duration: &durpb.Duration{Seconds: int64(123), Nanos: 234567000}, 617 Tags: pbutil.StringPairs("k1", "v1", "k2", "v2"), 618 VariantHash: variantHash, 619 TestMetadata: &pb.TestMetadata{ 620 Name: "name", 621 Location: &pb.TestLocation{ 622 Repo: "https://chromium.googlesource.com/chromium/src", 623 FileName: "//artifact_dir/a_test.cc", 624 Line: 54, 625 }, 626 BugComponent: &pb.BugComponent{ 627 System: &pb.BugComponent_Monorail{ 628 Monorail: &pb.MonorailComponent{ 629 Project: "chromium", 630 Value: "Component>Value", 631 }, 632 }, 633 }, 634 }, 635 FailureReason: &pb.FailureReason{ 636 PrimaryErrorMessage: "an error message", 637 Errors: []*pb.FailureReason_Error{ 638 {Message: "an error message"}, 639 {Message: "an error message2"}, 640 }, 641 TruncatedErrorsCount: 0, 642 }, 643 Properties: &structpb.Struct{ 644 Fields: map[string]*structpb.Value{ 645 "key": structpb.NewStringValue("value"), 646 }, 647 }, 648 SkipReason: pb.SkipReason_SKIP_REASON_UNSPECIFIED, 649 } 650 So(pbutil.ValidateTestResult(testclock.TestRecentTimeUTC, testResult), ShouldBeNil) 651 652 expected := &pb.TestResult{ 653 Name: name, 654 TestId: testID, 655 ResultId: resultID, 656 Expected: true, 657 Status: pb.TestStatus_PASS, 658 StartTime: ×tamppb.Timestamp{Seconds: 1000, Nanos: 1234}, 659 Duration: &durpb.Duration{Seconds: int64(123), Nanos: 234567000}, 660 VariantHash: variantHash, 661 FailureReason: &pb.FailureReason{ 662 PrimaryErrorMessage: "an error message", 663 Errors: []*pb.FailureReason_Error{ 664 {Message: "an error message"}, 665 {Message: "an error message2"}, 666 }, 667 TruncatedErrorsCount: 0, 668 }, 669 IsMasked: true, 670 SkipReason: pb.SkipReason_SKIP_REASON_UNSPECIFIED, 671 } 672 So(pbutil.ValidateTestResult(testclock.TestRecentTimeUTC, expected), ShouldBeNil) 673 674 err := ToLimitedData(ctx, testResult) 675 So(err, ShouldBeNil) 676 So(testResult, ShouldResembleProto, expected) 677 }) 678 679 Convey(`truncates primary error message`, func() { 680 testResult := &pb.TestResult{ 681 Name: name, 682 TestId: testID, 683 ResultId: resultID, 684 Variant: variant, 685 Expected: false, 686 Status: pb.TestStatus_FAIL, 687 SummaryHtml: "SummaryHtml", 688 StartTime: ×tamppb.Timestamp{Seconds: 1000, Nanos: 1234}, 689 Duration: &durpb.Duration{Seconds: int64(123), Nanos: 234567000}, 690 Tags: pbutil.StringPairs("k1", "v1", "k2", "v2"), 691 VariantHash: variantHash, 692 TestMetadata: &pb.TestMetadata{ 693 Name: "name", 694 Location: &pb.TestLocation{ 695 Repo: "https://chromium.googlesource.com/chromium/src", 696 FileName: "//artifact_dir/a_test.cc", 697 Line: 54, 698 }, 699 BugComponent: &pb.BugComponent{ 700 System: &pb.BugComponent_Monorail{ 701 Monorail: &pb.MonorailComponent{ 702 Project: "chromium", 703 Value: "Component>Value", 704 }, 705 }, 706 }, 707 }, 708 FailureReason: &pb.FailureReason{ 709 PrimaryErrorMessage: strings.Repeat("a very long error message", 10), 710 Errors: []*pb.FailureReason_Error{ 711 { 712 Message: strings.Repeat("a very long error message", 713 10), 714 }, 715 { 716 Message: strings.Repeat("a very long error message2", 717 10), 718 }, 719 }, 720 TruncatedErrorsCount: 0, 721 }, 722 Properties: &structpb.Struct{ 723 Fields: map[string]*structpb.Value{ 724 "key": structpb.NewStringValue("value"), 725 }, 726 }, 727 SkipReason: pb.SkipReason_SKIP_REASON_UNSPECIFIED, 728 } 729 So(pbutil.ValidateTestResult(testclock.TestRecentTimeUTC, testResult), ShouldBeNil) 730 731 limitedLongErrMsg := strings.Repeat("a very long error message", 732 10)[:limitedReasonLength] + "..." 733 limitedLongErrMsg2 := strings.Repeat("a very long error message2", 734 10)[:limitedReasonLength] + "..." 735 expected := &pb.TestResult{ 736 Name: name, 737 TestId: testID, 738 ResultId: resultID, 739 Expected: false, 740 Status: pb.TestStatus_FAIL, 741 StartTime: ×tamppb.Timestamp{Seconds: 1000, Nanos: 1234}, 742 Duration: &durpb.Duration{Seconds: int64(123), Nanos: 234567000}, 743 VariantHash: variantHash, 744 FailureReason: &pb.FailureReason{ 745 PrimaryErrorMessage: limitedLongErrMsg, 746 Errors: []*pb.FailureReason_Error{ 747 {Message: limitedLongErrMsg}, 748 {Message: limitedLongErrMsg2}, 749 }, 750 TruncatedErrorsCount: 0, 751 }, 752 IsMasked: true, 753 SkipReason: pb.SkipReason_SKIP_REASON_UNSPECIFIED, 754 } 755 So(pbutil.ValidateTestResult(testclock.TestRecentTimeUTC, expected), ShouldBeNil) 756 757 err := ToLimitedData(ctx, testResult) 758 So(err, ShouldBeNil) 759 So(testResult, ShouldResembleProto, expected) 760 }) 761 Convey(`mask preserves skip reason`, func() { 762 testResult := &pb.TestResult{ 763 Name: name, 764 TestId: testID, 765 ResultId: resultID, 766 Variant: variant, 767 Expected: true, 768 Status: pb.TestStatus_SKIP, 769 VariantHash: variantHash, 770 SkipReason: pb.SkipReason_AUTOMATICALLY_DISABLED_FOR_FLAKINESS, 771 } 772 So(pbutil.ValidateTestResult(testclock.TestRecentTimeUTC, testResult), ShouldBeNil) 773 774 expected := &pb.TestResult{ 775 Name: name, 776 TestId: testID, 777 ResultId: resultID, 778 Expected: true, 779 Status: pb.TestStatus_SKIP, 780 VariantHash: variantHash, 781 IsMasked: true, 782 SkipReason: pb.SkipReason_AUTOMATICALLY_DISABLED_FOR_FLAKINESS, 783 } 784 So(pbutil.ValidateTestResult(testclock.TestRecentTimeUTC, expected), ShouldBeNil) 785 786 err := ToLimitedData(ctx, testResult) 787 So(err, ShouldBeNil) 788 So(testResult, ShouldResembleProto, expected) 789 }) 790 }) 791 }