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  }