github.com/GoogleCloudPlatform/testgrid@v0.0.174/pkg/updater/resultstore/resultstore.go (about) 1 /* 2 Copyright 2023 The TestGrid Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // Package resultstore fetches and process results from ResultStore. 18 package resultstore 19 20 import ( 21 "context" 22 "fmt" 23 "regexp" 24 "sort" 25 "strings" 26 "time" 27 28 "github.com/GoogleCloudPlatform/testgrid/pkg/updater" 29 "github.com/GoogleCloudPlatform/testgrid/pkg/updater/resultstore/query" 30 "github.com/GoogleCloudPlatform/testgrid/util/gcs" 31 "github.com/sirupsen/logrus" 32 33 "github.com/GoogleCloudPlatform/testgrid/pb/config" 34 configpb "github.com/GoogleCloudPlatform/testgrid/pb/config" 35 cepb "github.com/GoogleCloudPlatform/testgrid/pb/custom_evaluator" 36 statepb "github.com/GoogleCloudPlatform/testgrid/pb/state" 37 statuspb "github.com/GoogleCloudPlatform/testgrid/pb/test_status" 38 timestamppb "github.com/golang/protobuf/ptypes/timestamp" 39 resultstorepb "google.golang.org/genproto/googleapis/devtools/resultstore/v2" 40 ) 41 42 // check if interface is implemented correctly 43 var _ updater.TargetResult = &singleActionResult{} 44 45 // TestResultStatus represents the status of a test result. 46 type TestResultStatus int64 47 48 // shouldUpdate returns whether the ResultStore updater should update this test group. 49 func shouldUpdate(log logrus.FieldLogger, tg *configpb.TestGroup, client *DownloadClient) bool { 50 if tg.GetResultSource().GetResultstoreConfig() == nil { 51 log.Debug("Skipping non-ResultStore group.") 52 return false 53 } 54 if client == nil { 55 log.WithField("name", tg.GetName()).Error("ResultStore update requested, but no client found.") 56 return false 57 } 58 return true 59 } 60 61 // Updater returns a ResultStore-based GroupUpdater, which knows how to process result data stored in ResultStore. 62 func Updater(resultStoreClient *DownloadClient, gcsClient gcs.Client, groupTimeout time.Duration, write bool) updater.GroupUpdater { 63 return func(parent context.Context, log logrus.FieldLogger, client gcs.Client, tg *configpb.TestGroup, gridPath gcs.Path) (bool, error) { 64 if !shouldUpdate(log, tg, resultStoreClient) { 65 return false, nil 66 } 67 ctx, cancel := context.WithTimeout(parent, groupTimeout) 68 defer cancel() 69 columnReader := ColumnReader(resultStoreClient, 0) 70 reprocess := 20 * time.Minute // allow 20m for prow to finish uploading artifacts 71 return updater.InflateDropAppend(ctx, log, gcsClient, tg, gridPath, write, columnReader, reprocess) 72 } 73 } 74 75 type singleActionResult struct { 76 TargetProto *resultstorepb.Target 77 ConfiguredTargetProto *resultstorepb.ConfiguredTarget 78 ActionProto *resultstorepb.Action 79 } 80 81 // make singleActionResult satisfy TargetResult interface 82 func (sar *singleActionResult) TargetStatus() statuspb.TestStatus { 83 status := convertStatus[sar.ConfiguredTargetProto.GetStatusAttributes().GetStatus()] 84 return status 85 } 86 87 func (sar *singleActionResult) extractHeaders(headerConf *configpb.TestGroup_ColumnHeader) []string { 88 if sar == nil { 89 return nil 90 } 91 92 var headers []string 93 94 if key := headerConf.GetProperty(); key != "" { 95 tr := &testResult{sar.ActionProto.GetTestAction().GetTestSuite(), nil} 96 for _, p := range tr.properties() { 97 if p.GetKey() == key { 98 headers = append(headers, p.GetValue()) 99 } 100 } 101 } 102 103 return headers 104 } 105 106 type multiActionResult struct { 107 TargetProto *resultstorepb.Target 108 ConfiguredTargetProto *resultstorepb.ConfiguredTarget 109 ActionProtos []*resultstorepb.Action 110 } 111 112 // invocation is an internal invocation representation which contains 113 // actual invocation data and results for each target 114 type invocation struct { 115 InvocationProto *resultstorepb.Invocation 116 TargetResults map[string][]*singleActionResult 117 } 118 119 func (inv *invocation) extractHeaders(headerConf *configpb.TestGroup_ColumnHeader) []string { 120 if inv == nil { 121 return nil 122 } 123 124 var headers []string 125 126 if key := headerConf.GetConfigurationValue(); key != "" { 127 for _, prop := range inv.InvocationProto.GetProperties() { 128 if prop.GetKey() == key { 129 headers = append(headers, prop.GetValue()) 130 } 131 } 132 } else if prefix := headerConf.GetLabel(); prefix != "" { 133 for _, label := range inv.InvocationProto.GetInvocationAttributes().GetLabels() { 134 if strings.HasPrefix(label, prefix) { 135 headers = append(headers, label[len(prefix):]) 136 } 137 } 138 } 139 return headers 140 } 141 142 // extractGroupID extracts grouping ID for a results based on the testgroup grouping configuration 143 // Returns an empty string for no config or incorrect config 144 func extractGroupID(tg *configpb.TestGroup, inv *invocation) string { 145 switch { 146 // P - build info 147 case inv == nil: 148 return "" 149 case tg.GetPrimaryGrouping() == configpb.TestGroup_PRIMARY_GROUPING_BUILD: 150 return identifyBuild(tg, inv) 151 default: 152 return inv.InvocationProto.GetId().GetInvocationId() 153 } 154 } 155 156 // ColumnReader fetches results since last update from ResultStore and translates them into columns. 157 func ColumnReader(client *DownloadClient, reprocess time.Duration) updater.ColumnReader { 158 return func(ctx context.Context, log logrus.FieldLogger, tg *configpb.TestGroup, oldCols []updater.InflatedColumn, defaultStop time.Time, receivers chan<- updater.InflatedColumn) error { 159 stop := updateStop(log, tg, time.Now(), oldCols, defaultStop, reprocess) 160 ids, err := search(ctx, log, client, tg.GetResultSource().GetResultstoreConfig(), stop) 161 if err != nil { 162 return fmt.Errorf("error searching invocations: %v", err) 163 } 164 invocationErrors := make(map[string]error) 165 var results []*FetchResult 166 for _, id := range ids { 167 result, invErr := client.FetchInvocation(ctx, log, id) 168 if invErr != nil { 169 invocationErrors[id] = invErr 170 continue 171 } 172 results = append(results, result) 173 } 174 175 invocations := processRawResults(log, results) 176 177 // Reverse-sort invocations by start time. 178 sort.SliceStable(invocations, func(i, j int) bool { 179 return invocations[i].InvocationProto.GetTiming().GetStartTime().GetSeconds() > invocations[j].InvocationProto.GetTiming().GetStartTime().GetSeconds() 180 }) 181 182 groups := groupInvocations(log, tg, invocations) 183 for _, group := range groups { 184 inflatedCol := processGroup(tg, group) 185 receivers <- *inflatedCol 186 } 187 return nil 188 } 189 } 190 191 // cellMessageIcon attempts to find an interesting message and icon by looking at the associated properties and tags. 192 func cellMessageIcon(annotations []*configpb.TestGroup_TestAnnotation, properties map[string][]string, tags []string) (string, string) { 193 check := make(map[string]string, len(properties)) 194 for k, v := range properties { 195 check[k] = v[0] 196 } 197 198 tagMap := make(map[string]string, len(annotations)) 199 200 for _, a := range annotations { 201 n := a.GetPropertyName() 202 check[n] = a.ShortText 203 tagMap[n] = a.ShortText 204 } 205 206 for _, a := range annotations { 207 n := a.GetPropertyName() 208 icon, ok := check[n] 209 if !ok { 210 continue 211 } 212 values, ok := properties[n] 213 if !ok || len(values) == 0 { 214 continue 215 } 216 return values[0], icon 217 } 218 219 for _, tag := range tags { 220 if icon, ok := tagMap[tag]; ok { 221 return tag, icon 222 } 223 } 224 return "", "" 225 } 226 227 func numericIcon(current *string, properties map[string][]string, key string) { 228 if properties == nil || key == "" { 229 return 230 } 231 vals, ok := properties[key] 232 if !ok { 233 return 234 } 235 mean := updater.Means(map[string][]string{key: vals})[key] 236 *current = fmt.Sprintf("%f", mean) 237 } 238 239 // invocationGroup will contain info on the groupId and all invocations for that group 240 // a group will correspond to a column after transformation 241 type invocationGroup struct { 242 GroupID string 243 Invocations []*invocation 244 } 245 246 // groupInvocations will group the invocations according to the grouping strategy in the config. 247 // groups will be reverse sorted by their latest invocation start time 248 // [inv1,inv2,inv3,inv4] -> [[inv1,inv2,inv3], [inv4]] 249 func groupInvocations(log logrus.FieldLogger, tg *configpb.TestGroup, invocations []*invocation) []*invocationGroup { 250 groupedInvocations := make(map[string]*invocationGroup) 251 252 var sortedGroups []*invocationGroup 253 254 for _, invocation := range invocations { 255 groupIdentifier := extractGroupID(tg, invocation) 256 group, ok := groupedInvocations[groupIdentifier] 257 if !ok { 258 group = &invocationGroup{ 259 GroupID: groupIdentifier, 260 } 261 groupedInvocations[groupIdentifier] = group 262 } 263 group.Invocations = append(group.Invocations, invocation) 264 } 265 266 for _, group := range groupedInvocations { 267 sortedGroups = append(sortedGroups, group) 268 } 269 270 // reverse sort groups by invocation time 271 sort.SliceStable(sortedGroups, func(i, j int) bool { 272 return sortedGroups[i].Invocations[0].InvocationProto.GetTiming().GetStartTime().GetSeconds() > sortedGroups[j].Invocations[0].InvocationProto.GetTiming().GetStartTime().GetSeconds() 273 }) 274 275 return sortedGroups 276 } 277 278 func processRawResults(log logrus.FieldLogger, results []*FetchResult) []*invocation { 279 var invs []*invocation 280 for _, result := range results { 281 inv := processRawResult(log, result) 282 invs = append(invs, inv) 283 } 284 return invs 285 } 286 287 // processRawResult converts raw FetchResult to invocation with single action/target result/configured target result per targetID 288 // Will skip processing any entries without Target or ConfiguredTarget 289 func processRawResult(log logrus.FieldLogger, result *FetchResult) *invocation { 290 291 multiActionResults := collateRawResults(log, result) 292 singleActionResults := isolateActions(log, multiActionResults) 293 294 return &invocation{result.Invocation, singleActionResults} 295 } 296 297 // collateRawResults collates targets, configured targets and multiple actions into a single structure using targetID as a key 298 func collateRawResults(log logrus.FieldLogger, result *FetchResult) map[string]*multiActionResult { 299 multiActionResults := make(map[string]*multiActionResult) 300 for _, target := range result.Targets { 301 trID := target.GetId().GetTargetId() 302 tr, ok := multiActionResults[trID] 303 if !ok { 304 tr = &multiActionResult{} 305 multiActionResults[trID] = tr 306 } else if tr.TargetProto != nil { 307 logrus.WithField("id", trID).Debug("Found duplicate target where not expected.") 308 } 309 tr.TargetProto = target 310 } 311 for _, configuredTarget := range result.ConfiguredTargets { 312 trID := configuredTarget.GetId().GetTargetId() 313 tr, ok := multiActionResults[trID] 314 if !ok { 315 tr = &multiActionResult{} 316 multiActionResults[trID] = tr 317 logrus.WithField("id", trID).Debug("Configured target doesn't have corresponding target?") 318 } else if tr.ConfiguredTargetProto != nil { 319 logrus.WithField("id", trID).Debug("Found duplicate configured target where not expected.") 320 } 321 tr.ConfiguredTargetProto = configuredTarget 322 } 323 for _, action := range result.Actions { 324 trID := action.GetId().GetTargetId() 325 tr, ok := multiActionResults[trID] 326 if !ok { 327 tr = &multiActionResult{} 328 multiActionResults[trID] = tr 329 logrus.WithField("id", trID).Debug("Action doesn't have corresponding target or configured target?") 330 } 331 tr.ActionProtos = append(tr.ActionProtos, action) 332 } 333 return multiActionResults 334 } 335 336 // isolateActions splits multiActionResults into one per action 337 // Any entries without Target or ConfiguredTarget will be skipped 338 func isolateActions(log logrus.FieldLogger, multiActionResults map[string]*multiActionResult) map[string][]*singleActionResult { 339 singleActionResults := make(map[string][]*singleActionResult) 340 for trID, multitr := range multiActionResults { 341 if multitr == nil || multitr.TargetProto == nil || multitr.ConfiguredTargetProto == nil { 342 logrus.WithField("id", trID).WithField("rawTargetResult", multitr).Debug("Missing something from rawTargetResult entry.") 343 continue 344 } 345 // no actions for some reason 346 if multitr.ActionProtos == nil { 347 tr := &singleActionResult{multitr.TargetProto, multitr.ConfiguredTargetProto, nil} 348 singleActionResults[trID] = append(singleActionResults[trID], tr) 349 } 350 for _, action := range multitr.ActionProtos { 351 tr := &singleActionResult{multitr.TargetProto, multitr.ConfiguredTargetProto, action} 352 singleActionResults[trID] = append(singleActionResults[trID], tr) 353 } 354 } 355 return singleActionResults 356 } 357 358 func timestampMilliseconds(t *timestamppb.Timestamp) float64 { 359 return float64(t.GetSeconds())*1000.0 + float64(t.GetNanos())/1000.0 360 } 361 362 var convertStatus = map[resultstorepb.Status]statuspb.TestStatus{ 363 resultstorepb.Status_STATUS_UNSPECIFIED: statuspb.TestStatus_NO_RESULT, 364 resultstorepb.Status_BUILDING: statuspb.TestStatus_RUNNING, 365 resultstorepb.Status_BUILT: statuspb.TestStatus_BUILD_PASSED, 366 resultstorepb.Status_FAILED_TO_BUILD: statuspb.TestStatus_BUILD_FAIL, 367 resultstorepb.Status_TESTING: statuspb.TestStatus_RUNNING, 368 resultstorepb.Status_PASSED: statuspb.TestStatus_PASS, 369 resultstorepb.Status_FAILED: statuspb.TestStatus_FAIL, 370 resultstorepb.Status_TIMED_OUT: statuspb.TestStatus_TIMED_OUT, 371 resultstorepb.Status_CANCELLED: statuspb.TestStatus_CANCEL, 372 resultstorepb.Status_TOOL_FAILED: statuspb.TestStatus_TOOL_FAIL, 373 resultstorepb.Status_INCOMPLETE: statuspb.TestStatus_UNKNOWN, 374 resultstorepb.Status_FLAKY: statuspb.TestStatus_FLAKY, 375 resultstorepb.Status_UNKNOWN: statuspb.TestStatus_UNKNOWN, 376 resultstorepb.Status_SKIPPED: statuspb.TestStatus_PASS_WITH_SKIPS, 377 } 378 379 // customTargetStatus will determine the overridden status based on custom evaluator rule set 380 func customTargetStatus(ruleSet *cepb.RuleSet, sar *singleActionResult) *statuspb.TestStatus { 381 return updater.CustomTargetStatus(ruleSet.GetRules(), sar) 382 } 383 384 // includeStatus determines if the single action result should be included based on config 385 func includeStatus(tg *configpb.TestGroup, sar *singleActionResult) bool { 386 status := convertStatus[sar.ConfiguredTargetProto.GetStatusAttributes().GetStatus()] 387 if status == statuspb.TestStatus_NO_RESULT { 388 return false 389 } 390 if status == statuspb.TestStatus_BUILD_PASSED && tg.IgnoreBuilt { 391 return false 392 } 393 if status == statuspb.TestStatus_RUNNING && tg.IgnorePending { 394 return false 395 } 396 if status == statuspb.TestStatus_PASS_WITH_SKIPS && tg.IgnoreSkip { 397 return false 398 } 399 return true 400 } 401 402 // testResult is a convenient representation of resultstore Test proto 403 // only one of those fields are set at any time for a testResult instance 404 type testResult struct { 405 suiteProto *resultstorepb.TestSuite 406 caseProto *resultstorepb.TestCase 407 } 408 409 // properties return the recursive list of properties for a particular testResult 410 func (t *testResult) properties() []*resultstorepb.Property { 411 var properties []*resultstorepb.Property 412 for _, p := range t.suiteProto.GetProperties() { 413 properties = append(properties, p) 414 } 415 for _, p := range t.caseProto.GetProperties() { 416 properties = append(properties, p) 417 } 418 419 for _, t := range t.suiteProto.GetTests() { 420 newTestResult := &testResult{t.GetTestSuite(), t.GetTestCase()} 421 properties = append(properties, newTestResult.properties()...) 422 } 423 return properties 424 } 425 426 // processGroup will convert grouped invocations into columns 427 func processGroup(tg *configpb.TestGroup, group *invocationGroup) *updater.InflatedColumn { 428 if group == nil || group.Invocations == nil { 429 return nil 430 } 431 methodLimit := testMethodLimit(tg) 432 matchMethods, unmatchMethods, matchMethodsErr, unmatchMethodsErr := testMethodRegex(tg) 433 434 col := &updater.InflatedColumn{ 435 Column: &statepb.Column{ 436 Name: group.GroupID, 437 }, 438 Cells: map[string]updater.Cell{}, 439 } 440 441 groupedCells := make(map[string][]updater.Cell) 442 443 hintTime := time.Unix(0, 0) 444 headers := make([][]string, len(tg.GetColumnHeader())) 445 446 // extract info from underlying invocations and target results 447 for _, invocation := range group.Invocations { 448 449 if build := identifyBuild(tg, invocation); build != "" { 450 col.Column.Build = build 451 } else { 452 col.Column.Build = group.GroupID 453 } 454 455 started := invocation.InvocationProto.GetTiming().GetStartTime() 456 resultStartTime := timestampMilliseconds(started) 457 if col.Column.Started == 0 || resultStartTime < col.Column.Started { 458 col.Column.Started = resultStartTime 459 } 460 461 if started.AsTime().After(hintTime) { 462 hintTime = started.AsTime() 463 } 464 465 for i, headerConf := range tg.GetColumnHeader() { 466 if invHeaders := invocation.extractHeaders(headerConf); invHeaders != nil { 467 headers[i] = append(headers[i], invHeaders...) 468 } 469 } 470 471 if err := matchMethodsErr; err != nil { 472 groupedCells["test_method_match_regex"] = append(groupedCells["test_method_match_regex"], 473 updater.Cell{ 474 Result: statuspb.TestStatus_TOOL_FAIL, 475 Message: err.Error(), 476 }) 477 } 478 479 if err := unmatchMethodsErr; err != nil { 480 groupedCells["test_method_unmatch_regex"] = append(groupedCells["test_method_unmatch_regex"], 481 updater.Cell{ 482 Result: statuspb.TestStatus_TOOL_FAIL, 483 Message: err.Error(), 484 }) 485 } 486 487 for targetID, singleActionResults := range invocation.TargetResults { 488 for _, sar := range singleActionResults { 489 if !includeStatus(tg, sar) { 490 continue 491 } 492 493 // assign status 494 status, ok := convertStatus[sar.ConfiguredTargetProto.GetStatusAttributes().GetStatus()] 495 if !ok { 496 status = statuspb.TestStatus_UNKNOWN 497 } 498 // TODO(sultan-duisenbay): sanitize build target and apply naming config 499 var cell updater.Cell 500 cell.CellID = invocation.InvocationProto.GetId().GetInvocationId() 501 cell.ID = targetID 502 cell.Result = status 503 if cr := customTargetStatus(tg.GetCustomEvaluatorRuleSet(), sar); cr != nil { 504 cell.Result = *cr 505 } 506 groupedCells[targetID] = append(groupedCells[targetID], cell) 507 testResults := getTestResults(sar.ActionProto.GetTestAction().GetTestSuite()) 508 testResults, filtered := filterResults(testResults, tg.GetTestMethodProperties(), matchMethods, unmatchMethods) 509 processTestResults(tg, groupedCells, testResults, sar, cell, targetID, methodLimit) 510 if filtered && len(testResults) == 0 { 511 continue 512 } 513 514 for i, headerConf := range tg.GetColumnHeader() { 515 if targetHeaders := sar.extractHeaders(headerConf); targetHeaders != nil { 516 headers[i] = append(headers[i], targetHeaders...) 517 } 518 } 519 520 cell.Metrics = calculateMetrics(sar) 521 522 // TODO (@bryanlou) check if we need to include properties from the target in addition to test cases 523 properties := map[string][]string{} 524 testSuite := sar.ActionProto.GetTestAction().GetTestSuite() 525 for _, t := range testSuite.GetTests() { 526 appendProperties(properties, t, nil) 527 } 528 } 529 } 530 531 for name, cells := range groupedCells { 532 split := updater.SplitCells(name, cells...) 533 for outName, outCell := range split { 534 col.Cells[outName] = outCell 535 } 536 } 537 } 538 539 hint, err := hintTime.MarshalText() 540 if err != nil { 541 hint = []byte{} 542 } 543 544 col.Column.Hint = string(hint) 545 col.Column.Extra = compileHeaders(tg.GetColumnHeader(), headers) 546 547 return col 548 } 549 550 // calculateMetrics calculates the numeric metrics (properties), test results 551 // and a target for singleActionResult and stores the duration in a map 552 func calculateMetrics(sar *singleActionResult) map[string]float64 { 553 properties := map[string][]string{} 554 testResultProperties(properties, sar.ActionProto.GetTestAction().GetTestSuite()) 555 numerics := updater.Means(properties) 556 targetElapsed := sar.TargetProto.GetTiming().GetDuration().AsDuration() 557 if targetElapsed > 0 { 558 numerics[updater.ElapsedKey] = targetElapsed.Minutes() 559 } 560 561 if dur := testResultDuration(sar.ActionProto.GetTestAction().GetTestSuite()); dur > 0 { 562 numerics[updater.TestMethodsElapsedKey] = dur.Minutes() 563 } 564 565 return numerics 566 } 567 568 // testResultProperties recursively inserts all result and its children's properties into the map. 569 func testResultProperties(properties map[string][]string, suite *resultstorepb.TestSuite) { 570 571 if suite == nil { 572 return 573 } 574 575 // add parent suite properties 576 for _, p := range suite.GetProperties() { 577 properties[p.GetKey()] = append(properties[p.GetKey()], p.GetValue()) 578 } 579 580 // add test case properties 581 for _, test := range suite.GetTests() { 582 if tc := test.GetTestCase(); tc != nil { 583 for _, p := range tc.GetProperties() { 584 properties[p.GetKey()] = append(properties[p.GetKey()], p.GetValue()) 585 } 586 } else { 587 testResultProperties(properties, test.GetTestSuite()) 588 } 589 } 590 } 591 592 // testResultDuration calculates the overall duration of test results. 593 func testResultDuration(suite *resultstorepb.TestSuite) time.Duration { 594 var totalDur time.Duration 595 if suite == nil { 596 return totalDur 597 } 598 599 if dur := suite.GetTiming().GetDuration().AsDuration(); dur > 0 { 600 return dur 601 } 602 603 for _, test := range suite.GetTests() { 604 if tc := test.GetTestCase(); tc != nil { 605 totalDur += tc.GetTiming().GetDuration().AsDuration() 606 } else { 607 totalDur += testResultDuration(test.GetTestSuite()) 608 } 609 } 610 return totalDur 611 } 612 613 // filterProperties returns the subset of results containing all the specified properties. 614 func filterProperties(results []*resultstorepb.Test, properties []*configpb.TestGroup_KeyValue) []*resultstorepb.Test { 615 if len(properties) == 0 { 616 return results 617 } 618 619 var out []*resultstorepb.Test 620 621 match := make(map[string]bool, len(properties)) 622 623 for _, p := range properties { 624 match[p.Key] = true 625 } 626 627 for _, r := range results { 628 found := map[string]string{} 629 fillProperties(found, r, match) 630 var miss bool 631 for _, p := range properties { 632 if found[p.Key] != p.Value { 633 miss = true 634 break 635 } 636 } 637 if miss { 638 continue 639 } 640 out = append(out, r) 641 } 642 return out 643 } 644 645 // fillProperties reduces the appendProperties result to the single value for each key or "*" if a key has multiple values. 646 func fillProperties(properties map[string]string, result *resultstorepb.Test, match map[string]bool) { 647 if result == nil { 648 return 649 } 650 multiProps := map[string][]string{} 651 652 appendProperties(multiProps, result, match) 653 654 for key, values := range multiProps { 655 if len(values) > 1 { 656 var diff bool 657 for _, v := range values { 658 if v != values[0] { 659 properties[key] = "*" 660 diff = true 661 break 662 } 663 } 664 if diff { 665 continue 666 } 667 } 668 properties[key] = values[0] 669 } 670 } 671 672 // appendProperties from result and its children into a map, optionally filtering to specific matching keys. 673 func appendProperties(properties map[string][]string, result *resultstorepb.Test, match map[string]bool) { 674 if result == nil { 675 return 676 } 677 678 for _, p := range result.GetTestCase().GetProperties() { 679 key := p.Key 680 if match != nil && !match[key] { 681 continue 682 } 683 properties[key] = append(properties[key], p.Value) 684 } 685 testResults := getTestResults(result.GetTestSuite()) 686 for _, r := range testResults { 687 appendProperties(properties, r, match) 688 } 689 } 690 691 // matchResults returns the subset of results with matching / without unmatching names. 692 func matchResults(results []*resultstorepb.Test, match, unmatch *regexp.Regexp) []*resultstorepb.Test { 693 if match == nil && unmatch == nil { 694 return results 695 } 696 var out []*resultstorepb.Test 697 for _, r := range results { 698 if match != nil && !match.MatchString(r.GetTestCase().CaseName) { 699 continue 700 } 701 if unmatch != nil && unmatch.MatchString(r.GetTestCase().CaseName) { 702 continue 703 } 704 out = append(out, r) 705 } 706 return out 707 } 708 709 // filterResults returns the subset of results and whether or not filtering was applied. 710 func filterResults(results []*resultstorepb.Test, properties []*config.TestGroup_KeyValue, match, unmatch *regexp.Regexp) ([]*resultstorepb.Test, bool) { 711 results = filterProperties(results, properties) 712 results = matchResults(results, match, unmatch) 713 filtered := len(properties) > 0 || match != nil || unmatch != nil 714 return results, filtered 715 } 716 717 // getTestResults traverses through a test suite and returns a list of all tests 718 // that exists inside of it. 719 func getTestResults(testSuite *resultstorepb.TestSuite) []*resultstorepb.Test { 720 if testSuite == nil { 721 return nil 722 } 723 724 if len(testSuite.GetTests()) == 0 { 725 return []*resultstorepb.Test{ 726 { 727 TestType: &resultstorepb.Test_TestSuite{TestSuite: testSuite}, 728 }, 729 } 730 } 731 732 var tests []*resultstorepb.Test 733 for _, test := range testSuite.GetTests() { 734 if test.GetTestCase() != nil { 735 tests = append(tests, test) 736 } else { 737 tests = append(tests, getTestResults(test.GetTestSuite())...) 738 } 739 } 740 return tests 741 } 742 743 func testMethodLimit(tg *configpb.TestGroup) int { 744 var testMethodLimit int 745 const defaultTestMethodLimit = 20 746 if tg == nil { 747 return 0 748 } 749 if tg.EnableTestMethods { 750 testMethodLimit = int(tg.MaxTestMethodsPerTest) 751 if testMethodLimit == 0 { 752 testMethodLimit = defaultTestMethodLimit 753 } 754 } 755 return testMethodLimit 756 } 757 758 func testMethodRegex(tg *configpb.TestGroup) (matchMethods *regexp.Regexp, unmatchMethods *regexp.Regexp, matchMethodsErr error, unmatchMethodsErr error) { 759 if tg == nil { 760 return 761 } 762 if m := tg.GetTestMethodMatchRegex(); m != "" { 763 matchMethods, matchMethodsErr = regexp.Compile(m) 764 } 765 if um := tg.GetTestMethodUnmatchRegex(); um != "" { 766 unmatchMethods, unmatchMethodsErr = regexp.Compile(um) 767 } 768 return 769 } 770 771 func mapStatusToCellResult(testCase *resultstorepb.TestCase) statuspb.TestStatus { 772 res := testCase.GetResult() 773 switch { 774 case strings.HasPrefix(testCase.CaseName, "DISABLED_"): 775 return statuspb.TestStatus_PASS_WITH_SKIPS 776 case res == resultstorepb.TestCase_SKIPPED: 777 return statuspb.TestStatus_PASS_WITH_SKIPS 778 case res == resultstorepb.TestCase_SUPPRESSED: 779 return statuspb.TestStatus_PASS_WITH_SKIPS 780 case res == resultstorepb.TestCase_CANCELLED: 781 return statuspb.TestStatus_CANCEL 782 case res == resultstorepb.TestCase_INTERRUPTED: 783 return statuspb.TestStatus_CANCEL 784 case len(testCase.Failures) > 0 || len(testCase.Errors) > 0: 785 return statuspb.TestStatus_FAIL 786 default: 787 return statuspb.TestStatus_PASS 788 } 789 } 790 791 // processTestResults iterates through a list of test results and adds them to 792 // a map of groupedcells based on the method name produced 793 func processTestResults(tg *config.TestGroup, groupedCells map[string][]updater.Cell, testResults []*resultstorepb.Test, sar *singleActionResult, cell updater.Cell, targetID string, testMethodLimit int) { 794 tags := sar.TargetProto.GetTargetAttributes().GetTags() 795 testSuite := sar.ActionProto.GetTestAction().GetTestSuite() 796 shortTextMetric := tg.GetShortTextMetric() 797 for _, testResult := range testResults { 798 var methodName string 799 properties := map[string][]string{} 800 for _, t := range testSuite.GetTests() { 801 appendProperties(properties, t, nil) 802 } 803 if testResult.GetTestCase() != nil { 804 methodName = testResult.GetTestCase().GetCaseName() 805 } else { 806 methodName = testResult.GetTestSuite().GetSuiteName() 807 } 808 809 if tg.UseFullMethodNames { 810 parts := strings.Split(testResult.GetTestCase().GetClassName(), ".") 811 className := parts[len(parts)-1] 812 methodName = className + "." + methodName 813 } 814 815 methodName = targetID + "@TESTGRID@" + methodName 816 817 trCell := updater.Cell{ 818 ID: targetID, // same targetID as the parent TargetResult 819 CellID: cell.CellID, // same cellID 820 Result: mapStatusToCellResult(testResult.GetTestCase()), 821 } 822 823 trCell.Message, trCell.Icon = cellMessageIcon(tg.TestAnnotations, properties, tags) 824 numericIcon(&trCell.Icon, properties, shortTextMetric) 825 826 if trCell.Result == statuspb.TestStatus_PASS_WITH_SKIPS && tg.IgnoreSkip { 827 continue 828 } 829 if len(testResults) <= testMethodLimit { 830 groupedCells[methodName] = append(groupedCells[methodName], trCell) 831 } 832 } 833 } 834 835 // compileHeaders reduces all seen header values down to the final string value. 836 // Separates multiple values with || when configured, otherwise the value becomes * 837 func compileHeaders(columnHeader []*configpb.TestGroup_ColumnHeader, headers [][]string) []string { 838 if len(columnHeader) == 0 { 839 return nil 840 } 841 842 var compiledHeaders []string 843 for i, headerList := range headers { 844 switch { 845 case len(headerList) == 0: 846 compiledHeaders = append(compiledHeaders, "") 847 case len(headerList) == 1: 848 compiledHeaders = append(compiledHeaders, headerList[0]) 849 case columnHeader[i].GetListAllValues(): 850 var values []string 851 for _, value := range headerList { 852 values = append(values, value) 853 } 854 sort.Strings(values) 855 compiledHeaders = append(compiledHeaders, strings.Join(values, "||")) 856 default: 857 compiledHeaders = append(compiledHeaders, "*") 858 } 859 } 860 return compiledHeaders 861 } 862 863 // identifyBuild applies build override configurations and assigns a build 864 // Returns an empty string if no configurations are present or no configs are correctly set. 865 // i.e. no key is found in properties. 866 func identifyBuild(tg *configpb.TestGroup, inv *invocation) string { 867 switch { 868 case tg.GetBuildOverrideConfigurationValue() != "": 869 key := tg.GetBuildOverrideConfigurationValue() 870 for _, property := range inv.InvocationProto.GetProperties() { 871 if property.GetKey() == key { 872 return property.GetValue() 873 } 874 } 875 return "" 876 case tg.GetBuildOverrideStrftime() != "": 877 layout := updater.FormatStrftime(tg.BuildOverrideStrftime) 878 timing := inv.InvocationProto.GetTiming().GetStartTime() 879 startTime := time.Unix(timing.Seconds, int64(timing.Nanos)).UTC() 880 return startTime.Format(layout) 881 default: 882 return "" 883 } 884 } 885 886 func queryAfter(query string, when time.Time) string { 887 if query == "" { 888 return "" 889 } 890 return fmt.Sprintf("%s timing.start_time>=\"%s\"", query, when.UTC().Format(time.RFC3339)) 891 } 892 893 const ( 894 // Use this when searching invocations, e.g. if query does not search for a target. 895 prowLabel = `invocation_attributes.labels:"prow"` 896 // Use this when searching for a configured target, e.g. if query contains `target:"<target>"`. 897 prowTargetLabel = `invocation.invocation_attributes.labels:"prow"` 898 ) 899 900 func queryProw(baseQuery string, stop time.Time) (string, error) { 901 // TODO: ResultStore use is assumed to be Prow-only at the moment. Make this more flexible in future. 902 if baseQuery == "" { 903 return queryAfter(prowLabel, stop), nil 904 } 905 query, err := query.TranslateQuery(baseQuery) 906 if err != nil { 907 return "", err 908 } 909 return queryAfter(fmt.Sprintf("%s %s", query, prowTargetLabel), stop), nil 910 } 911 912 func search(ctx context.Context, log logrus.FieldLogger, client *DownloadClient, rsConfig *configpb.ResultStoreConfig, stop time.Time) ([]string, error) { 913 if client == nil { 914 return nil, fmt.Errorf("no ResultStore client provided") 915 } 916 query, err := queryProw(rsConfig.GetQuery(), stop) 917 if err != nil { 918 return nil, fmt.Errorf("queryProw() failed to create query: %v", err) 919 } 920 log.WithField("query", query).Debug("Searching ResultStore.") 921 // Quit if search goes over 5 minutes. 922 ctx, cancel := context.WithTimeout(ctx, 5*time.Minute) 923 defer cancel() 924 ids, err := client.Search(ctx, log, query, rsConfig.GetProject()) 925 log.WithField("ids", len(ids)).WithError(err).Debug("Searched ResultStore.") 926 return ids, err 927 } 928 929 func mostRecent(times []time.Time) time.Time { 930 var max time.Time 931 for _, t := range times { 932 if t.After(max) { 933 max = t 934 } 935 } 936 return max 937 } 938 939 func stopFromColumns(log logrus.FieldLogger, cols []updater.InflatedColumn) time.Time { 940 var stop time.Time 941 for _, col := range cols { 942 log = log.WithField("start", col.Column.Started).WithField("hint", col.Column.Hint) 943 startedMillis := col.Column.Started 944 if startedMillis == 0 { 945 continue 946 } 947 started := time.Unix(int64(startedMillis/1000), 0) 948 949 var hint time.Time 950 if err := hint.UnmarshalText([]byte(col.Column.Hint)); col.Column.Hint != "" && err != nil { 951 log.WithError(err).Warning("Could not parse hint, ignoring.") 952 } 953 stop = mostRecent([]time.Time{started, hint, stop}) 954 } 955 return stop.Truncate(time.Second) // We don't need sub-second resolution. 956 } 957 958 // updateStop returns the time to stop searching after, given previous columns and a default. 959 func updateStop(log logrus.FieldLogger, tg *configpb.TestGroup, now time.Time, oldCols []updater.InflatedColumn, defaultStop time.Time, reprocess time.Duration) time.Time { 960 hint := stopFromColumns(log, oldCols) 961 // Process at most twice days_of_results. 962 days := tg.GetDaysOfResults() 963 if days == 0 { 964 days = 1 965 } 966 max := now.AddDate(0, 0, -2*int(days)) 967 968 stop := mostRecent([]time.Time{hint, defaultStop, max}) 969 970 // Process at least the reprocess threshold. 971 if reprocessTime := now.Add(-1 * reprocess); stop.After(reprocessTime) { 972 stop = reprocessTime 973 } 974 975 // Primary grouping can sometimes miss recent results, mitigate by extending the stop. 976 if tg.GetPrimaryGrouping() == configpb.TestGroup_PRIMARY_GROUPING_BUILD { 977 stop.Add(-30 * time.Minute) 978 } 979 980 return stop.Truncate(time.Second) // We don't need sub-second resolution. 981 }