go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/pbutil/test_result_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 pbutil 16 17 import ( 18 "fmt" 19 "regexp" 20 "strings" 21 "testing" 22 "time" 23 24 . "github.com/smartystreets/goconvey/convey" 25 "google.golang.org/protobuf/types/known/durationpb" 26 "google.golang.org/protobuf/types/known/structpb" 27 "google.golang.org/protobuf/types/known/timestamppb" 28 29 "go.chromium.org/luci/common/clock/testclock" 30 . "go.chromium.org/luci/common/testing/assertions" 31 pb "go.chromium.org/luci/resultdb/proto/v1" 32 ) 33 34 // validTestResult returns a valid TestResult sample. 35 func validTestResult(now time.Time) *pb.TestResult { 36 st := timestamppb.New(now.Add(-2 * time.Minute)) 37 return &pb.TestResult{ 38 Name: "invocations/a/tests/invocation_id1/results/result_id1", 39 TestId: "this is testID", 40 ResultId: "result_id1", 41 Variant: Variant("a", "b"), 42 Expected: true, 43 Status: pb.TestStatus_PASS, 44 SummaryHtml: "HTML summary", 45 StartTime: st, 46 Duration: durationpb.New(time.Minute), 47 TestMetadata: &pb.TestMetadata{ 48 Location: &pb.TestLocation{ 49 Repo: "https://git.example.com", 50 FileName: "//a_test.go", 51 Line: 54, 52 }, 53 BugComponent: &pb.BugComponent{ 54 System: &pb.BugComponent_Monorail{ 55 Monorail: &pb.MonorailComponent{ 56 Project: "chromium", 57 Value: "Component>Value", 58 }, 59 }, 60 }, 61 }, 62 Tags: StringPairs("k1", "v1"), 63 } 64 } 65 66 // fieldDoesNotMatch returns the string of unspecified error with the field name. 67 func fieldUnspecified(fieldName string) string { 68 return fmt.Sprintf("%s: %s", fieldName, unspecified()) 69 } 70 71 // fieldDoesNotMatch returns the string of doesNotMatch error with the field name. 72 func fieldDoesNotMatch(fieldName string, re *regexp.Regexp) string { 73 return fmt.Sprintf("%s: %s", fieldName, doesNotMatch(re)) 74 } 75 76 func TestTestResultName(t *testing.T) { 77 t.Parallel() 78 79 Convey("ParseTestResultName", t, func() { 80 Convey("Parse", func() { 81 invID, testID, resultID, err := ParseTestResultName( 82 "invocations/a/tests/ninja:%2F%2Fchrome%2Ftest:foo_tests%2FBarTest.DoBaz/results/result5") 83 So(err, ShouldBeNil) 84 So(invID, ShouldEqual, "a") 85 So(testID, ShouldEqual, "ninja://chrome/test:foo_tests/BarTest.DoBaz") 86 So(resultID, ShouldEqual, "result5") 87 }) 88 89 Convey("Invalid", func() { 90 Convey(`has slashes`, func() { 91 _, _, _, err := ParseTestResultName( 92 "invocations/inv/tests/ninja://test/results/result1") 93 So(err, ShouldErrLike, doesNotMatch(testResultNameRe)) 94 }) 95 96 Convey(`bad unescape`, func() { 97 _, _, _, err := ParseTestResultName( 98 "invocations/a/tests/bad_hex_%gg/results/result1") 99 So(err, ShouldErrLike, "test id") 100 }) 101 102 Convey(`unescaped unprintable`, func() { 103 _, _, _, err := ParseTestResultName( 104 "invocations/a/tests/unprintable_%07/results/result1") 105 So(err, ShouldErrLike, "non-printable rune") 106 }) 107 }) 108 109 Convey("Format", func() { 110 So(TestResultName("a", "ninja://chrome/test:foo_tests/BarTest.DoBaz", "result5"), 111 ShouldEqual, 112 "invocations/a/tests/ninja:%2F%2Fchrome%2Ftest:foo_tests%2FBarTest.DoBaz/results/result5") 113 }) 114 }) 115 } 116 117 func TestValidateTestResult(t *testing.T) { 118 t.Parallel() 119 now := testclock.TestRecentTimeUTC 120 validate := func(result *pb.TestResult) error { 121 return ValidateTestResult(now, result) 122 } 123 124 Convey("Succeeds", t, func() { 125 msg := validTestResult(now) 126 So(validate(msg), ShouldBeNil) 127 128 Convey("with unicode TestID", func() { 129 // Uses printable unicode character 'µ'. 130 msg.TestId = "TestVariousDeadlines/5µs" 131 So(ValidateTestID(msg.TestId), ShouldErrLike, nil) 132 So(validate(msg), ShouldBeNil) 133 }) 134 135 Convey("with invalid Name", func() { 136 // ValidateTestResult should skip validating TestResult.Name. 137 msg.Name = "this is not a valid name for TestResult.Name" 138 So(ValidateTestResultName(msg.Name), ShouldErrLike, doesNotMatch(testResultNameRe)) 139 So(validate(msg), ShouldBeNil) 140 }) 141 142 Convey("with no variant", func() { 143 msg.Variant = nil 144 So(validate(msg), ShouldBeNil) 145 }) 146 147 Convey("with valid summary", func() { 148 msg.SummaryHtml = strings.Repeat("1", maxLenSummaryHTML) 149 So(validate(msg), ShouldBeNil) 150 }) 151 152 Convey("with empty tags", func() { 153 msg.Tags = nil 154 So(validate(msg), ShouldBeNil) 155 }) 156 157 Convey("with nil start_time", func() { 158 msg.StartTime = nil 159 So(validate(msg), ShouldBeNil) 160 }) 161 162 Convey("with nil duration", func() { 163 msg.Duration = nil 164 So(validate(msg), ShouldBeNil) 165 }) 166 167 Convey("with valid properties", func() { 168 msg.Properties = &structpb.Struct{ 169 Fields: map[string]*structpb.Value{ 170 "key": structpb.NewStringValue("value"), 171 }, 172 } 173 So(validate(msg), ShouldBeNil) 174 }) 175 176 Convey("with skip reason", func() { 177 msg.Status = pb.TestStatus_SKIP 178 msg.SkipReason = pb.SkipReason_AUTOMATICALLY_DISABLED_FOR_FLAKINESS 179 So(validate(msg), ShouldBeNil) 180 }) 181 }) 182 183 Convey("Fails", t, func() { 184 msg := validTestResult(now) 185 Convey("with nil", func() { 186 So(validate(nil), ShouldErrLike, unspecified()) 187 }) 188 189 Convey("with empty TestID", func() { 190 msg.TestId = "" 191 So(validate(msg), ShouldErrLike, fieldUnspecified("test_id")) 192 }) 193 194 Convey("with invalid TestID", func() { 195 badInputs := []string{ 196 strings.Repeat("1", 512+1), 197 // [[:print:]] matches with [ -~] and [[:graph:]] 198 string(rune(7)), 199 string("cafe\u0301"), // UTF8 text that is not in normalization form C. 200 } 201 for _, in := range badInputs { 202 msg.TestId = in 203 So(validate(msg), ShouldErrLike, "") 204 } 205 }) 206 207 Convey("with empty ResultID", func() { 208 msg.ResultId = "" 209 So(validate(msg), ShouldErrLike, fieldUnspecified("result_id")) 210 }) 211 212 Convey("with invalid ResultID", func() { 213 badInputs := []string{ 214 strings.Repeat("1", 32+1), 215 string(rune(7)), 216 } 217 for _, in := range badInputs { 218 msg.ResultId = in 219 So(validate(msg), ShouldErrLike, fieldDoesNotMatch("result_id", resultIDRe)) 220 } 221 }) 222 223 Convey("with invalid Variant", func() { 224 badInputs := []*pb.Variant{ 225 Variant("", ""), 226 Variant("", "val"), 227 } 228 for _, in := range badInputs { 229 msg.Variant = in 230 So(validate(msg), ShouldErrLike, fieldUnspecified("key")) 231 } 232 }) 233 234 Convey("with invalid Status", func() { 235 msg.Status = pb.TestStatus(len(pb.TestStatus_name) + 1) 236 So(validate(msg), ShouldErrLike, "status: invalid value") 237 }) 238 239 Convey("with STATUS_UNSPECIFIED", func() { 240 msg.Status = pb.TestStatus_STATUS_UNSPECIFIED 241 So(validate(msg), ShouldErrLike, "status: cannot be STATUS_UNSPECIFIED") 242 }) 243 244 Convey("with skip reason but not skip status", func() { 245 msg.Status = pb.TestStatus_ABORT 246 msg.SkipReason = pb.SkipReason_AUTOMATICALLY_DISABLED_FOR_FLAKINESS 247 So(validate(msg), ShouldErrLike, "skip_reason: value must be zero (UNSPECIFIED) when status is not SKIP") 248 }) 249 250 Convey("with too big summary", func() { 251 msg.SummaryHtml = strings.Repeat("☕", maxLenSummaryHTML) 252 So(validate(msg), ShouldErrLike, "summary_html: exceeds the maximum size") 253 }) 254 255 Convey("with invalid StartTime and Duration", func() { 256 Convey("because start_time is in the future", func() { 257 future := timestamppb.New(now.Add(time.Hour)) 258 msg.StartTime = future 259 So(validate(msg), ShouldErrLike, fmt.Sprintf("start_time: cannot be > (now + %s)", clockSkew)) 260 }) 261 262 Convey("because duration is < 0", func() { 263 msg.Duration = durationpb.New(-1 * time.Minute) 264 So(validate(msg), ShouldErrLike, "duration: is < 0") 265 }) 266 267 Convey("because (start_time + duration) is in the future", func() { 268 st := timestamppb.New(now.Add(-1 * time.Hour)) 269 msg.StartTime = st 270 msg.Duration = durationpb.New(2 * time.Hour) 271 expected := fmt.Sprintf("start_time + duration: cannot be > (now + %s)", clockSkew) 272 So(validate(msg), ShouldErrLike, expected) 273 }) 274 }) 275 276 Convey("with invalid StringPairs", func() { 277 msg.Tags = StringPairs("", "") 278 So(validate(msg), ShouldErrLike, `"":"": key: unspecified`) 279 }) 280 281 Convey("Test metadata", func() { 282 Convey("filename", func() { 283 Convey("unspecified", func() { 284 msg.TestMetadata.Location.FileName = "" 285 So(validate(msg), ShouldErrLike, "test_metadata: location: file_name: unspecified") 286 }) 287 Convey("too long", func() { 288 msg.TestMetadata.Location.FileName = "//" + strings.Repeat("super long", 100) 289 So(validate(msg), ShouldErrLike, "test_metadata: location: file_name: length exceeds 512") 290 }) 291 Convey("no double slashes", func() { 292 msg.TestMetadata.Location.FileName = "file_name" 293 So(validate(msg), ShouldErrLike, "test_metadata: location: file_name: doesn't start with //") 294 }) 295 Convey("back slash", func() { 296 msg.TestMetadata.Location.FileName = "//dir\\file" 297 So(validate(msg), ShouldErrLike, "test_metadata: location: file_name: has \\") 298 }) 299 Convey("trailing slash", func() { 300 msg.TestMetadata.Location.FileName = "//file_name/" 301 So(validate(msg), ShouldErrLike, "test_metadata: location: file_name: ends with /") 302 }) 303 }) 304 Convey("line", func() { 305 msg.TestMetadata.Location.Line = -1 306 So(validate(msg), ShouldErrLike, "test_metadata: location: line: must not be negative") 307 }) 308 Convey("repo", func() { 309 msg.TestMetadata.Location.Repo = "https://chromium.googlesource.com/chromium/src.git" 310 So(validate(msg), ShouldErrLike, "test_metadata: location: repo: must not end with .git") 311 }) 312 313 Convey("no location and no bug component", func() { 314 msg.TestMetadata = &pb.TestMetadata{Name: "name"} 315 So(validate(msg), ShouldBeNil) 316 }) 317 Convey("location no repo", func() { 318 msg.TestMetadata = &pb.TestMetadata{ 319 Name: "name", 320 Location: &pb.TestLocation{ 321 FileName: "//file_name", 322 }, 323 } 324 So(validate(msg), ShouldErrLike, "test_metadata: location: repo: required") 325 }) 326 327 Convey("nil bug system in bug component", func() { 328 msg.TestMetadata = &pb.TestMetadata{ 329 Name: "name", 330 BugComponent: &pb.BugComponent{ 331 System: nil, 332 }, 333 } 334 So(validate(msg), ShouldErrLike, "bug system is required for bug components") 335 }) 336 Convey("valid monorail bug component", func() { 337 msg.TestMetadata = &pb.TestMetadata{ 338 Name: "name", 339 BugComponent: &pb.BugComponent{ 340 System: &pb.BugComponent_Monorail{ 341 Monorail: &pb.MonorailComponent{ 342 Project: "1chromium1", 343 Value: "Component>Value", 344 }, 345 }, 346 }, 347 } 348 So(validate(msg), ShouldBeNil) 349 }) 350 Convey("wrong size monorail bug component value", func() { 351 msg.TestMetadata = &pb.TestMetadata{ 352 Name: "name", 353 BugComponent: &pb.BugComponent{ 354 System: &pb.BugComponent_Monorail{ 355 Monorail: &pb.MonorailComponent{ 356 Project: "chromium", 357 Value: strings.Repeat("a", 601), 358 }, 359 }, 360 }, 361 } 362 So(validate(msg), ShouldErrLike, "monorail.value: is invalid") 363 }) 364 Convey("invalid monorail bug component value", func() { 365 msg.TestMetadata = &pb.TestMetadata{ 366 Name: "name", 367 BugComponent: &pb.BugComponent{ 368 System: &pb.BugComponent_Monorail{ 369 Monorail: &pb.MonorailComponent{ 370 Project: "chromium", 371 Value: "Component<><>Value", 372 }, 373 }, 374 }, 375 } 376 So(validate(msg), ShouldErrLike, "monorail.value: is invalid") 377 }) 378 Convey("wrong size monorail bug component project", func() { 379 msg.TestMetadata = &pb.TestMetadata{ 380 Name: "name", 381 BugComponent: &pb.BugComponent{ 382 System: &pb.BugComponent_Monorail{ 383 Monorail: &pb.MonorailComponent{ 384 Project: strings.Repeat("a", 64), 385 Value: "Component>Value", 386 }, 387 }, 388 }, 389 } 390 So(validate(msg), ShouldErrLike, "monorail.project: is invalid") 391 }) 392 Convey("using invalid characters in monorail bug component project", func() { 393 msg.TestMetadata = &pb.TestMetadata{ 394 Name: "name", 395 BugComponent: &pb.BugComponent{ 396 System: &pb.BugComponent_Monorail{ 397 Monorail: &pb.MonorailComponent{ 398 Project: "$%^ $$^%", 399 Value: "Component>Value", 400 }, 401 }, 402 }, 403 } 404 So(validate(msg), ShouldErrLike, "monorail.project: is invalid") 405 }) 406 Convey("using only numbers in monorail bug component project", func() { 407 msg.TestMetadata = &pb.TestMetadata{ 408 Name: "name", 409 BugComponent: &pb.BugComponent{ 410 System: &pb.BugComponent_Monorail{ 411 Monorail: &pb.MonorailComponent{ 412 Project: "11111", 413 Value: "Component>Value", 414 }, 415 }, 416 }, 417 } 418 So(validate(msg), ShouldErrLike, "monorail.project: is invalid") 419 }) 420 Convey("valid buganizer component", func() { 421 msg.TestMetadata = &pb.TestMetadata{ 422 Name: "name", 423 BugComponent: &pb.BugComponent{ 424 System: &pb.BugComponent_IssueTracker{ 425 IssueTracker: &pb.IssueTrackerComponent{ 426 ComponentId: 1234, 427 }, 428 }, 429 }, 430 } 431 So(validate(msg), ShouldBeNil) 432 }) 433 Convey("invalid buganizer component id", func() { 434 msg.TestMetadata = &pb.TestMetadata{ 435 Name: "name", 436 BugComponent: &pb.BugComponent{ 437 System: &pb.BugComponent_IssueTracker{ 438 IssueTracker: &pb.IssueTrackerComponent{ 439 ComponentId: -1, 440 }, 441 }, 442 }, 443 } 444 So(validate(msg), ShouldErrLike, "issue_tracker.component_id: is invalid") 445 }) 446 Convey("with too big properties", func() { 447 msg.TestMetadata = &pb.TestMetadata{ 448 PropertiesSchema: "package.message", 449 Properties: &structpb.Struct{ 450 Fields: map[string]*structpb.Value{ 451 "key": structpb.NewStringValue(strings.Repeat("1", MaxSizeProperties)), 452 }, 453 }, 454 } 455 So(validate(msg), ShouldErrLike, "properties: exceeds the maximum size") 456 }) 457 Convey("no properties_schema with non-empty properties", func() { 458 msg.TestMetadata = &pb.TestMetadata{ 459 Properties: &structpb.Struct{ 460 Fields: map[string]*structpb.Value{ 461 "key": structpb.NewStringValue("1"), 462 }, 463 }, 464 } 465 So(validate(msg), ShouldErrLike, "properties_schema must be specified with non-empty properties") 466 }) 467 Convey("invalid properties_schema", func() { 468 msg.TestMetadata = &pb.TestMetadata{ 469 PropertiesSchema: "package", 470 } 471 So(validate(msg), ShouldErrLike, "properties_schema: does not match") 472 }) 473 Convey("valid properties_schema and non-empty properties", func() { 474 msg.TestMetadata = &pb.TestMetadata{ 475 PropertiesSchema: "package.message", 476 Properties: &structpb.Struct{ 477 Fields: map[string]*structpb.Value{ 478 "key": structpb.NewStringValue("1"), 479 }, 480 }, 481 } 482 So(validate(msg), ShouldBeNil) 483 }) 484 }) 485 486 Convey("with too big properties", func() { 487 msg.Properties = &structpb.Struct{ 488 Fields: map[string]*structpb.Value{ 489 "key": structpb.NewStringValue(strings.Repeat("1", MaxSizeProperties)), 490 }, 491 } 492 So(validate(msg), ShouldErrLike, "properties: exceeds the maximum size") 493 }) 494 495 Convey("Validate failure reason", func() { 496 errorMessage1 := "error1" 497 errorMessage2 := "error2" 498 longErrorMessage := strings.Repeat("a very long error message", 100) 499 Convey("valid failure reason", func() { 500 msg.FailureReason = &pb.FailureReason{ 501 PrimaryErrorMessage: errorMessage1, 502 Errors: []*pb.FailureReason_Error{ 503 {Message: errorMessage1}, 504 {Message: errorMessage2}, 505 }, 506 TruncatedErrorsCount: 0, 507 } 508 So(validate(msg), ShouldBeNil) 509 }) 510 511 Convey("primary_error_message exceeds the maximum limit", func() { 512 msg.FailureReason = &pb.FailureReason{ 513 PrimaryErrorMessage: longErrorMessage, 514 } 515 So(validate(msg), ShouldErrLike, "primary_error_message: "+ 516 "exceeds the maximum") 517 }) 518 519 Convey("one of the error messages exceeds the maximum limit", func() { 520 msg.FailureReason = &pb.FailureReason{ 521 PrimaryErrorMessage: errorMessage1, 522 Errors: []*pb.FailureReason_Error{ 523 {Message: errorMessage1}, 524 {Message: longErrorMessage}, 525 }, 526 TruncatedErrorsCount: 0, 527 } 528 So(validate(msg), ShouldErrLike, 529 "errors[1]: message: exceeds the maximum size of 1024 "+ 530 "bytes") 531 }) 532 533 Convey("the first error doesn't match primary_error_message", func() { 534 msg.FailureReason = &pb.FailureReason{ 535 PrimaryErrorMessage: errorMessage1, 536 Errors: []*pb.FailureReason_Error{ 537 {Message: errorMessage2}, 538 }, 539 TruncatedErrorsCount: 0, 540 } 541 So(validate(msg), ShouldErrLike, 542 "errors[0]: message: must match primary_error_message") 543 }) 544 545 Convey("the total size of the errors list exceeds the limit", func() { 546 maxErrorMessage := strings.Repeat(".", 1024) 547 msg.FailureReason = &pb.FailureReason{ 548 PrimaryErrorMessage: maxErrorMessage, 549 Errors: []*pb.FailureReason_Error{ 550 {Message: maxErrorMessage}, 551 {Message: maxErrorMessage}, 552 {Message: maxErrorMessage}, 553 {Message: maxErrorMessage}, 554 }, 555 TruncatedErrorsCount: 1, 556 } 557 So(validate(msg), ShouldErrLike, 558 "errors: exceeds the maximum total size of 3172 bytes") 559 }) 560 561 Convey("invalid truncated error count", func() { 562 msg.FailureReason = &pb.FailureReason{ 563 PrimaryErrorMessage: errorMessage1, 564 Errors: []*pb.FailureReason_Error{ 565 {Message: errorMessage1}, 566 {Message: errorMessage2}, 567 }, 568 TruncatedErrorsCount: -1, 569 } 570 So(validate(msg), ShouldErrLike, "truncated_errors_count: "+ 571 "must be non-negative") 572 }) 573 }) 574 }) 575 }