github.com/GoogleCloudPlatform/testgrid@v0.0.174/pkg/updater/resultstore/resultstore_test.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  	"sync"
    25  	"testing"
    26  	"time"
    27  
    28  	configpb "github.com/GoogleCloudPlatform/testgrid/pb/config"
    29  	evalpb "github.com/GoogleCloudPlatform/testgrid/pb/custom_evaluator"
    30  	statepb "github.com/GoogleCloudPlatform/testgrid/pb/state"
    31  	teststatuspb "github.com/GoogleCloudPlatform/testgrid/pb/test_status"
    32  	"github.com/GoogleCloudPlatform/testgrid/pkg/updater"
    33  	durationpb "github.com/golang/protobuf/ptypes/duration"
    34  	timestamppb "github.com/golang/protobuf/ptypes/timestamp"
    35  	"github.com/google/go-cmp/cmp"
    36  	"github.com/google/go-cmp/cmp/cmpopts"
    37  	"github.com/sirupsen/logrus"
    38  	"google.golang.org/genproto/googleapis/devtools/resultstore/v2"
    39  	"google.golang.org/grpc"
    40  	"google.golang.org/protobuf/testing/protocmp"
    41  )
    42  
    43  type fakeClient struct {
    44  	searches    map[string][]string
    45  	invocations map[string]FetchResult
    46  }
    47  
    48  func (c *fakeClient) search(query string) ([]string, error) {
    49  	notFound := fmt.Errorf("no results found for %q", query)
    50  	if c.searches == nil {
    51  		return nil, notFound
    52  	}
    53  	invocationIDs, ok := c.searches[query]
    54  	if !ok {
    55  		return nil, notFound
    56  	}
    57  	return invocationIDs, nil
    58  }
    59  
    60  func (c *fakeClient) SearchInvocations(ctx context.Context, req *resultstore.SearchInvocationsRequest, opts ...grpc.CallOption) (*resultstore.SearchInvocationsResponse, error) {
    61  	invocationIDs, err := c.search(req.GetQuery())
    62  	if err != nil {
    63  		return nil, err
    64  	}
    65  	var invocations []*resultstore.Invocation
    66  	for _, invocationID := range invocationIDs {
    67  		invoc := &resultstore.Invocation{
    68  			Id: &resultstore.Invocation_Id{InvocationId: invocationID},
    69  		}
    70  		invocations = append(invocations, invoc)
    71  	}
    72  	return &resultstore.SearchInvocationsResponse{Invocations: invocations}, nil
    73  }
    74  
    75  func (c *fakeClient) SearchConfiguredTargets(ctx context.Context, req *resultstore.SearchConfiguredTargetsRequest, opts ...grpc.CallOption) (*resultstore.SearchConfiguredTargetsResponse, error) {
    76  	invocationIDs, err := c.search(req.GetQuery())
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  	var configuredTargets []*resultstore.ConfiguredTarget
    81  	for _, invocationID := range invocationIDs {
    82  		configuredTarget := &resultstore.ConfiguredTarget{
    83  			Id: &resultstore.ConfiguredTarget_Id{InvocationId: invocationID},
    84  		}
    85  		configuredTargets = append(configuredTargets, configuredTarget)
    86  	}
    87  	return &resultstore.SearchConfiguredTargetsResponse{ConfiguredTargets: configuredTargets}, nil
    88  }
    89  
    90  func (c *fakeClient) ExportInvocation(ctx context.Context, req *resultstore.ExportInvocationRequest, opts ...grpc.CallOption) (*resultstore.ExportInvocationResponse, error) {
    91  	notFound := fmt.Errorf("no result found for invocation %q", req.GetName())
    92  	if c.invocations == nil {
    93  		return nil, notFound
    94  	}
    95  	result, ok := c.invocations[req.GetName()]
    96  	if !ok {
    97  		return nil, notFound
    98  	}
    99  	return &resultstore.ExportInvocationResponse{
   100  		Invocation:        result.Invocation,
   101  		Actions:           result.Actions,
   102  		ConfiguredTargets: result.ConfiguredTargets,
   103  		Targets:           result.Targets,
   104  	}, nil
   105  }
   106  
   107  func invocationName(invocationID string) string {
   108  	return fmt.Sprintf("invocations/%s", invocationID)
   109  }
   110  
   111  func targetName(targetID, invocationID string) string {
   112  	return fmt.Sprintf("invocations/%s/targets/%s", invocationID, targetID)
   113  }
   114  
   115  func timeMustText(t time.Time) string {
   116  	s, err := t.MarshalText()
   117  	if err != nil {
   118  		panic("timeMustText() panicked")
   119  	}
   120  	return string(s)
   121  }
   122  
   123  func TestExtractGroupID(t *testing.T) {
   124  	cases := []struct {
   125  		name string
   126  		tg   *configpb.TestGroup
   127  		pr   *invocation
   128  		want string
   129  	}{
   130  		{
   131  			name: "nil",
   132  		},
   133  		{
   134  			name: "primary grouping BUILD by override config value",
   135  			tg: &configpb.TestGroup{
   136  				DaysOfResults:                   7,
   137  				BuildOverrideConfigurationValue: "test-key-1",
   138  				PrimaryGrouping:                 configpb.TestGroup_PRIMARY_GROUPING_BUILD,
   139  			},
   140  			pr: &invocation{
   141  				InvocationProto: &resultstore.Invocation{
   142  					Id: &resultstore.Invocation_Id{
   143  						InvocationId: "id-1",
   144  					},
   145  					Properties: []*resultstore.Property{
   146  						{
   147  							Key:   "test-key-1",
   148  							Value: "test-val-1",
   149  						},
   150  					},
   151  					Name: invocationName("id-1"),
   152  					Timing: &resultstore.Timing{
   153  						StartTime: &timestamppb.Timestamp{
   154  							Seconds: 1234,
   155  						},
   156  					},
   157  				},
   158  			},
   159  			want: "test-val-1",
   160  		},
   161  		{
   162  			name: "fallback grouping BUILD resort to default",
   163  			tg: &configpb.TestGroup{
   164  				DaysOfResults:                   7,
   165  				BuildOverrideConfigurationValue: "test-key-1",
   166  				FallbackGrouping:                configpb.TestGroup_FALLBACK_GROUPING_BUILD,
   167  			},
   168  			pr: &invocation{
   169  				InvocationProto: &resultstore.Invocation{
   170  					Id: &resultstore.Invocation_Id{
   171  						InvocationId: "id-1",
   172  					},
   173  					Properties: []*resultstore.Property{
   174  						{
   175  							Key:   "test-key-1",
   176  							Value: "test-val-1",
   177  						},
   178  					},
   179  					Name: invocationName("id-1"),
   180  					Timing: &resultstore.Timing{
   181  						StartTime: &timestamppb.Timestamp{
   182  							Seconds: 1234,
   183  						},
   184  					},
   185  				},
   186  			},
   187  			want: "id-1",
   188  		},
   189  	}
   190  	for _, tc := range cases {
   191  		t.Run(tc.name, func(t *testing.T) {
   192  			got := extractGroupID(tc.tg, tc.pr)
   193  			if diff := cmp.Diff(tc.want, got); diff != "" {
   194  				t.Errorf("extractGroupID() differed (-want, +got): %s", diff)
   195  			}
   196  		})
   197  	}
   198  }
   199  
   200  func TestColumnReader(t *testing.T) {
   201  	// We already have functions testing 'stop' logic.
   202  	// Scope this test to whether the column reader fetches and returns ascending results.
   203  	oneMonthConfig := &configpb.TestGroup{
   204  		Name:          "a-test-group",
   205  		DaysOfResults: 30,
   206  	}
   207  	now := time.Now()
   208  	oneDayAgo := now.AddDate(0, 0, -1)
   209  	twoDaysAgo := now.AddDate(0, 0, -2)
   210  	threeDaysAgo := now.AddDate(0, 0, -3)
   211  	oneMonthAgo := now.AddDate(0, 0, -30)
   212  	testQueryAfter := queryAfter(prowLabel, oneMonthAgo)
   213  	cases := []struct {
   214  		name    string
   215  		client  *fakeClient
   216  		tg      *configpb.TestGroup
   217  		want    []updater.InflatedColumn
   218  		wantErr bool
   219  	}{
   220  		{
   221  			name:    "empty",
   222  			tg:      oneMonthConfig,
   223  			wantErr: true,
   224  		},
   225  		{
   226  			name: "basic",
   227  			tg: &configpb.TestGroup{
   228  				DaysOfResults: 30,
   229  			},
   230  			client: &fakeClient{
   231  				searches: map[string][]string{
   232  					testQueryAfter: {"id-1", "id-2"},
   233  				},
   234  				invocations: map[string]FetchResult{
   235  					invocationName("id-1"): {
   236  						Invocation: &resultstore.Invocation{
   237  							Id: &resultstore.Invocation_Id{
   238  								InvocationId: "id-1",
   239  							},
   240  							Name: invocationName("id-1"),
   241  							Timing: &resultstore.Timing{
   242  								StartTime: &timestamppb.Timestamp{
   243  									Seconds: oneDayAgo.Unix(),
   244  								},
   245  							},
   246  						},
   247  						Targets: []*resultstore.Target{
   248  							{
   249  								Id: &resultstore.Target_Id{
   250  									TargetId: "tgt-id-1",
   251  								},
   252  								StatusAttributes: &resultstore.StatusAttributes{
   253  									Status: resultstore.Status_PASSED,
   254  								},
   255  							},
   256  						},
   257  						ConfiguredTargets: []*resultstore.ConfiguredTarget{
   258  							{
   259  								Id: &resultstore.ConfiguredTarget_Id{
   260  									TargetId: "tgt-id-1",
   261  								},
   262  								StatusAttributes: &resultstore.StatusAttributes{
   263  									Status: resultstore.Status_PASSED,
   264  								},
   265  							},
   266  						},
   267  						Actions: []*resultstore.Action{
   268  							{
   269  								Id: &resultstore.Action_Id{
   270  									TargetId: "tgt-id-1",
   271  									ActionId: "build",
   272  								},
   273  							},
   274  						},
   275  					},
   276  					invocationName("id-2"): {
   277  						Invocation: &resultstore.Invocation{
   278  							Id: &resultstore.Invocation_Id{
   279  								InvocationId: "id-2",
   280  							},
   281  							Name: invocationName("id-2"),
   282  							Timing: &resultstore.Timing{
   283  								StartTime: &timestamppb.Timestamp{
   284  									Seconds: twoDaysAgo.Unix(),
   285  								},
   286  							},
   287  						},
   288  						Targets: []*resultstore.Target{
   289  							{
   290  								Id: &resultstore.Target_Id{
   291  									TargetId: "tgt-id-1",
   292  								},
   293  								StatusAttributes: &resultstore.StatusAttributes{
   294  									Status: resultstore.Status_FAILED,
   295  								},
   296  							},
   297  						},
   298  						ConfiguredTargets: []*resultstore.ConfiguredTarget{
   299  							{
   300  								Id: &resultstore.ConfiguredTarget_Id{
   301  									TargetId: "tgt-id-1",
   302  								},
   303  								StatusAttributes: &resultstore.StatusAttributes{
   304  									Status: resultstore.Status_FAILED,
   305  								},
   306  							},
   307  						},
   308  						Actions: []*resultstore.Action{
   309  							{
   310  								Id: &resultstore.Action_Id{
   311  									TargetId: "tgt-id-1",
   312  									ActionId: "build",
   313  								},
   314  							},
   315  						},
   316  					},
   317  				},
   318  			},
   319  			want: []updater.InflatedColumn{
   320  				{
   321  					Column: &statepb.Column{
   322  						Build:   "id-1",
   323  						Name:    "id-1",
   324  						Started: float64(oneDayAgo.Unix() * 1000),
   325  						Hint:    timeMustText(oneDayAgo.Local().Truncate(time.Second)),
   326  					},
   327  					Cells: map[string]updater.Cell{
   328  						"tgt-id-1": {
   329  							ID:     "tgt-id-1",
   330  							CellID: "id-1",
   331  							Result: teststatuspb.TestStatus_PASS,
   332  						},
   333  					},
   334  				},
   335  				{
   336  					Column: &statepb.Column{
   337  						Build:   "id-2",
   338  						Name:    "id-2",
   339  						Started: float64(twoDaysAgo.Unix() * 1000),
   340  						Hint:    timeMustText(twoDaysAgo.Truncate(time.Second)),
   341  					},
   342  					Cells: map[string]updater.Cell{
   343  						"tgt-id-1": {
   344  							ID:     "tgt-id-1",
   345  							CellID: "id-2",
   346  							Result: teststatuspb.TestStatus_FAIL,
   347  						},
   348  					},
   349  				},
   350  			},
   351  		},
   352  		{
   353  			name: "no results from query",
   354  			tg:   oneMonthConfig,
   355  			client: &fakeClient{
   356  				searches: map[string][]string{},
   357  				invocations: map[string]FetchResult{
   358  					invocationName("id-1"): {
   359  						Invocation: &resultstore.Invocation{
   360  							Id: &resultstore.Invocation_Id{
   361  								InvocationId: "id-1",
   362  							},
   363  							Name: invocationName("id-1"),
   364  							Timing: &resultstore.Timing{
   365  								StartTime: &timestamppb.Timestamp{
   366  									Seconds: oneDayAgo.Unix(),
   367  								},
   368  							},
   369  						},
   370  					},
   371  					invocationName("id-2"): {
   372  						Invocation: &resultstore.Invocation{
   373  							Id: &resultstore.Invocation_Id{
   374  								InvocationId: "id-2",
   375  							},
   376  							Name: invocationName("id-2"),
   377  							Timing: &resultstore.Timing{
   378  								StartTime: &timestamppb.Timestamp{
   379  									Seconds: twoDaysAgo.Unix(),
   380  								},
   381  							},
   382  						},
   383  					},
   384  					invocationName("id-3"): {
   385  						Invocation: &resultstore.Invocation{
   386  							Id: &resultstore.Invocation_Id{
   387  								InvocationId: "id-3",
   388  							},
   389  							Name: invocationName("id-3"),
   390  							Timing: &resultstore.Timing{
   391  								StartTime: &timestamppb.Timestamp{
   392  									Seconds: threeDaysAgo.Unix(),
   393  								},
   394  							},
   395  						},
   396  					},
   397  				},
   398  			},
   399  			wantErr: true,
   400  		},
   401  		{
   402  			name: "no invocations found",
   403  			client: &fakeClient{
   404  				searches: map[string][]string{
   405  					testQueryAfter: {"id-2", "id-3", "id-1"},
   406  				},
   407  				invocations: map[string]FetchResult{},
   408  			},
   409  			want: nil,
   410  		},
   411  		{
   412  			name: "ids not in order",
   413  			client: &fakeClient{
   414  				searches: map[string][]string{
   415  					testQueryAfter: {"id-2", "id-3", "id-1"},
   416  				},
   417  				invocations: map[string]FetchResult{
   418  					invocationName("id-1"): {
   419  						Invocation: &resultstore.Invocation{
   420  							Id: &resultstore.Invocation_Id{
   421  								InvocationId: "id-1",
   422  							},
   423  							Name: invocationName("id-1"),
   424  							Timing: &resultstore.Timing{
   425  								StartTime: &timestamppb.Timestamp{
   426  									Seconds: oneDayAgo.Unix(),
   427  								},
   428  							},
   429  						},
   430  					},
   431  					invocationName("id-2"): {
   432  						Invocation: &resultstore.Invocation{
   433  							Id: &resultstore.Invocation_Id{
   434  								InvocationId: "id-2",
   435  							},
   436  							Name: invocationName("id-2"),
   437  							Timing: &resultstore.Timing{
   438  								StartTime: &timestamppb.Timestamp{
   439  									Seconds: twoDaysAgo.Unix(),
   440  								},
   441  							},
   442  						},
   443  					},
   444  					invocationName("id-3"): {
   445  						Invocation: &resultstore.Invocation{
   446  							Id: &resultstore.Invocation_Id{
   447  								InvocationId: "id-3",
   448  							},
   449  							Name: invocationName("id-3"),
   450  							Timing: &resultstore.Timing{
   451  								StartTime: &timestamppb.Timestamp{
   452  									Seconds: threeDaysAgo.Unix(),
   453  								},
   454  							},
   455  						},
   456  					},
   457  				},
   458  			},
   459  			want: []updater.InflatedColumn{
   460  				{
   461  					Column: &statepb.Column{
   462  						Build:   "id-1",
   463  						Name:    "id-1",
   464  						Started: float64(oneDayAgo.Unix() * 1000),
   465  						Hint:    timeMustText(oneDayAgo.Truncate(time.Second)),
   466  					},
   467  					Cells: map[string]updater.Cell{},
   468  				},
   469  				{
   470  					Column: &statepb.Column{
   471  						Build:   "id-2",
   472  						Name:    "id-2",
   473  						Started: float64(twoDaysAgo.Unix() * 1000),
   474  						Hint:    timeMustText(twoDaysAgo.Truncate(time.Second)),
   475  					},
   476  					Cells: map[string]updater.Cell{},
   477  				},
   478  				{
   479  					Column: &statepb.Column{
   480  						Build:   "id-3",
   481  						Name:    "id-3",
   482  						Started: float64(threeDaysAgo.Unix() * 1000),
   483  						Hint:    timeMustText(threeDaysAgo.Truncate(time.Second)),
   484  					},
   485  					Cells: map[string]updater.Cell{},
   486  				},
   487  			},
   488  		},
   489  	}
   490  	for _, tc := range cases {
   491  		t.Run(tc.name, func(t *testing.T) {
   492  			var dlClient *DownloadClient
   493  			if tc.client != nil {
   494  				dlClient = &DownloadClient{client: tc.client}
   495  			}
   496  			columnReader := ColumnReader(dlClient, 0)
   497  			var got []updater.InflatedColumn
   498  			ch := make(chan updater.InflatedColumn)
   499  			var wg sync.WaitGroup
   500  			wg.Add(1)
   501  			go func() {
   502  				defer wg.Done()
   503  				for col := range ch {
   504  					got = append(got, col)
   505  				}
   506  			}()
   507  			err := columnReader(context.Background(), logrus.WithField("case", tc.name), oneMonthConfig, nil, oneMonthAgo, ch)
   508  			close(ch)
   509  			wg.Wait()
   510  			if err != nil && !tc.wantErr {
   511  				t.Errorf("columnReader() errored: %v", err)
   512  			} else if err == nil && tc.wantErr {
   513  				t.Errorf("columnReader() did not error as expected")
   514  			}
   515  			if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" {
   516  				t.Errorf("columnReader() differed (-want, +got): %s", diff)
   517  			}
   518  		})
   519  	}
   520  }
   521  
   522  func TestCellMessageIcon(t *testing.T) {
   523  	cases := []struct {
   524  		name        string
   525  		annotations []*configpb.TestGroup_TestAnnotation
   526  		properties  map[string][]string
   527  		tags        []string
   528  		message     string
   529  		icon        string
   530  	}{
   531  		{
   532  			name: "basically works",
   533  		},
   534  		{
   535  			name: "find annotation from property",
   536  			annotations: []*configpb.TestGroup_TestAnnotation{
   537  				{
   538  					ShortText: "icon",
   539  					ShortTextMessageSource: &configpb.TestGroup_TestAnnotation_PropertyName{
   540  						PropertyName: "props",
   541  					},
   542  				},
   543  			},
   544  			properties: map[string][]string{
   545  				"props": {"first", "second"},
   546  			},
   547  			message: "first",
   548  			icon:    "icon",
   549  		},
   550  		{
   551  			name: "find annotation from tag",
   552  			annotations: []*configpb.TestGroup_TestAnnotation{
   553  				{
   554  					ShortText: "icon",
   555  					ShortTextMessageSource: &configpb.TestGroup_TestAnnotation_PropertyName{
   556  						PropertyName: "actually-a-tag",
   557  					},
   558  				},
   559  			},
   560  			tags:    []string{"actually-a-tag"},
   561  			message: "actually-a-tag",
   562  			icon:    "icon",
   563  		},
   564  		{
   565  			name: "find annotation from tag",
   566  			annotations: []*configpb.TestGroup_TestAnnotation{
   567  				{
   568  					ShortText: "icon",
   569  					ShortTextMessageSource: &configpb.TestGroup_TestAnnotation_PropertyName{
   570  						PropertyName: "actually-a-tag",
   571  					},
   572  				},
   573  				{
   574  					ShortText: "icon-2",
   575  					ShortTextMessageSource: &configpb.TestGroup_TestAnnotation_PropertyName{
   576  						PropertyName: "actually-a-tag-2",
   577  					},
   578  				},
   579  			},
   580  			tags:    []string{"actually-a-tag"},
   581  			message: "actually-a-tag",
   582  			icon:    "icon",
   583  		},
   584  	}
   585  
   586  	for _, tc := range cases {
   587  		message, icon := cellMessageIcon(tc.annotations, tc.properties, tc.tags)
   588  		if tc.message != message {
   589  			t.Errorf("cellMessageIcon() got unexpected message %q, want %q", message, tc.message)
   590  		}
   591  		if tc.icon != icon {
   592  			t.Errorf("cellMessageIcon() got unexpected icon %q, want %q", icon, tc.icon)
   593  		}
   594  	}
   595  }
   596  
   597  func TestTimestampMilliseconds(t *testing.T) {
   598  	cases := []struct {
   599  		name      string
   600  		timestamp *timestamppb.Timestamp
   601  		want      float64
   602  	}{
   603  		{
   604  			name:      "nil",
   605  			timestamp: nil,
   606  			want:      0,
   607  		},
   608  		{
   609  			name:      "zero",
   610  			timestamp: &timestamppb.Timestamp{},
   611  			want:      0,
   612  		},
   613  		{
   614  			name: "basic",
   615  			timestamp: &timestamppb.Timestamp{
   616  				Seconds: 1234,
   617  				Nanos:   5678,
   618  			},
   619  			want: 1234005.678,
   620  		},
   621  	}
   622  	for _, tc := range cases {
   623  		t.Run(tc.name, func(t *testing.T) {
   624  			got := timestampMilliseconds(tc.timestamp)
   625  			approx := cmpopts.EquateApprox(.01, 0)
   626  			if diff := cmp.Diff(tc.want, got, approx); diff != "" {
   627  				t.Errorf("timestampMilliseconds(%v) differed (-want, +got): %s", tc.timestamp, diff)
   628  			}
   629  		})
   630  	}
   631  }
   632  
   633  func TestProcessRawResult(t *testing.T) {
   634  	cases := []struct {
   635  		name   string
   636  		result *FetchResult
   637  		want   *invocation
   638  	}{
   639  		{
   640  			name: "just invocation",
   641  			result: &FetchResult{
   642  				Invocation: &resultstore.Invocation{
   643  					Name: invocationName("Best invocation"),
   644  					Id: &resultstore.Invocation_Id{
   645  						InvocationId: "uuid-222",
   646  					},
   647  				},
   648  			},
   649  			want: &invocation{
   650  				InvocationProto: &resultstore.Invocation{
   651  					Name: invocationName("Best invocation"),
   652  					Id: &resultstore.Invocation_Id{
   653  						InvocationId: "uuid-222",
   654  					},
   655  				},
   656  				TargetResults: make(map[string][]*singleActionResult),
   657  			},
   658  		},
   659  		{
   660  			name: "invocation + targets + configured targets",
   661  			result: &FetchResult{
   662  				Invocation: &resultstore.Invocation{
   663  					Name: invocationName("Best invocation"),
   664  					Id: &resultstore.Invocation_Id{
   665  						InvocationId: "uuid-222",
   666  					},
   667  				},
   668  				Targets: []*resultstore.Target{
   669  					{
   670  						Name: targetName("updater", "uuid-222"),
   671  						Id: &resultstore.Target_Id{
   672  							InvocationId: "uuid-222",
   673  							TargetId:     "tgt-uuid-1",
   674  						},
   675  					},
   676  					{
   677  						Name: targetName("tabulator", "uuid-222"),
   678  						Id: &resultstore.Target_Id{
   679  							InvocationId: "uuid-222",
   680  							TargetId:     "tgt-uuid-2",
   681  						},
   682  					},
   683  				},
   684  				ConfiguredTargets: []*resultstore.ConfiguredTarget{
   685  					{
   686  						Name: targetName("updater", "uuid-222"),
   687  						Id: &resultstore.ConfiguredTarget_Id{
   688  							InvocationId: "uuid-222",
   689  							TargetId:     "tgt-uuid-1",
   690  						},
   691  					},
   692  					{
   693  						Name: targetName("tabulator", "uuid-222"),
   694  						Id: &resultstore.ConfiguredTarget_Id{
   695  							InvocationId: "uuid-222",
   696  							TargetId:     "tgt-uuid-2",
   697  						},
   698  					},
   699  				},
   700  			},
   701  			want: &invocation{
   702  				InvocationProto: &resultstore.Invocation{
   703  					Name: invocationName("Best invocation"),
   704  					Id: &resultstore.Invocation_Id{
   705  						InvocationId: "uuid-222",
   706  					},
   707  				},
   708  				TargetResults: map[string][]*singleActionResult{
   709  					"tgt-uuid-1": {
   710  						{
   711  							TargetProto: &resultstore.Target{
   712  								Name: targetName("updater", "uuid-222"),
   713  								Id: &resultstore.Target_Id{
   714  									InvocationId: "uuid-222",
   715  									TargetId:     "tgt-uuid-1",
   716  								},
   717  							},
   718  							ConfiguredTargetProto: &resultstore.ConfiguredTarget{
   719  								Name: targetName("updater", "uuid-222"),
   720  								Id: &resultstore.ConfiguredTarget_Id{
   721  									InvocationId: "uuid-222",
   722  									TargetId:     "tgt-uuid-1",
   723  								},
   724  							},
   725  						},
   726  					},
   727  					"tgt-uuid-2": {
   728  						{
   729  							TargetProto: &resultstore.Target{
   730  								Name: targetName("tabulator", "uuid-222"),
   731  								Id: &resultstore.Target_Id{
   732  									InvocationId: "uuid-222",
   733  									TargetId:     "tgt-uuid-2",
   734  								},
   735  							},
   736  							ConfiguredTargetProto: &resultstore.ConfiguredTarget{
   737  								Name: targetName("tabulator", "uuid-222"),
   738  								Id: &resultstore.ConfiguredTarget_Id{
   739  									InvocationId: "uuid-222",
   740  									TargetId:     "tgt-uuid-2",
   741  								},
   742  							},
   743  						},
   744  					},
   745  				},
   746  			},
   747  		},
   748  		{
   749  			name: "all together + extra actions",
   750  			result: &FetchResult{
   751  				Invocation: &resultstore.Invocation{
   752  					Name: invocationName("Best invocation"),
   753  					Id: &resultstore.Invocation_Id{
   754  						InvocationId: "uuid-222",
   755  					},
   756  				},
   757  				Targets: []*resultstore.Target{
   758  					{
   759  						Name: "/testgrid/backend:updater",
   760  						Id: &resultstore.Target_Id{
   761  							InvocationId: "uuid-222",
   762  							TargetId:     "tgt-uuid-1",
   763  						},
   764  					},
   765  					{
   766  						Name: "/testgrid/backend:tabulator",
   767  						Id: &resultstore.Target_Id{
   768  							InvocationId: "uuid-222",
   769  							TargetId:     "tgt-uuid-2",
   770  						},
   771  					},
   772  				},
   773  				ConfiguredTargets: []*resultstore.ConfiguredTarget{
   774  					{
   775  						Name: "/testgrid/backend:updater",
   776  						Id: &resultstore.ConfiguredTarget_Id{
   777  							InvocationId: "uuid-222",
   778  							TargetId:     "tgt-uuid-1",
   779  						},
   780  					},
   781  					{
   782  						Name: "/testgrid/backend:tabulator",
   783  						Id: &resultstore.ConfiguredTarget_Id{
   784  							InvocationId: "uuid-222",
   785  							TargetId:     "tgt-uuid-2",
   786  						},
   787  					},
   788  				},
   789  				Actions: []*resultstore.Action{
   790  					{
   791  						Name: "/testgrid/backend:updater",
   792  						Id: &resultstore.Action_Id{
   793  							InvocationId: "uuid-222",
   794  							TargetId:     "tgt-uuid-1",
   795  							ActionId:     "flying",
   796  						},
   797  					},
   798  					{
   799  						Name: "/testgrid/backend:tabulator",
   800  						Id: &resultstore.Action_Id{
   801  							InvocationId: "uuid-222",
   802  							TargetId:     "tgt-uuid-2",
   803  							ActionId:     "walking",
   804  						},
   805  					},
   806  					{
   807  						Name: "/testgrid/backend:tabulator",
   808  						Id: &resultstore.Action_Id{
   809  							InvocationId: "uuid-222",
   810  							TargetId:     "tgt-uuid-2",
   811  							ActionId:     "flying",
   812  						},
   813  					},
   814  				},
   815  			},
   816  			want: &invocation{
   817  				InvocationProto: &resultstore.Invocation{
   818  					Name: invocationName("Best invocation"),
   819  					Id: &resultstore.Invocation_Id{
   820  						InvocationId: "uuid-222",
   821  					},
   822  				},
   823  				TargetResults: map[string][]*singleActionResult{
   824  					"tgt-uuid-1": {
   825  						{
   826  							TargetProto: &resultstore.Target{
   827  								Name: "/testgrid/backend:updater",
   828  								Id: &resultstore.Target_Id{
   829  									InvocationId: "uuid-222",
   830  									TargetId:     "tgt-uuid-1",
   831  								},
   832  							},
   833  							ConfiguredTargetProto: &resultstore.ConfiguredTarget{
   834  								Name: "/testgrid/backend:updater",
   835  								Id: &resultstore.ConfiguredTarget_Id{
   836  									InvocationId: "uuid-222",
   837  									TargetId:     "tgt-uuid-1",
   838  								},
   839  							},
   840  							ActionProto: &resultstore.Action{
   841  								Name: "/testgrid/backend:updater",
   842  								Id: &resultstore.Action_Id{
   843  									InvocationId: "uuid-222",
   844  									TargetId:     "tgt-uuid-1",
   845  									ActionId:     "flying",
   846  								},
   847  							},
   848  						},
   849  					},
   850  					"tgt-uuid-2": {
   851  						{
   852  							TargetProto: &resultstore.Target{
   853  								Name: "/testgrid/backend:tabulator",
   854  								Id: &resultstore.Target_Id{
   855  									InvocationId: "uuid-222",
   856  									TargetId:     "tgt-uuid-2",
   857  								},
   858  							},
   859  							ConfiguredTargetProto: &resultstore.ConfiguredTarget{
   860  								Name: "/testgrid/backend:tabulator",
   861  								Id: &resultstore.ConfiguredTarget_Id{
   862  									InvocationId: "uuid-222",
   863  									TargetId:     "tgt-uuid-2",
   864  								},
   865  							},
   866  							ActionProto: &resultstore.Action{
   867  								Name: "/testgrid/backend:tabulator",
   868  								Id: &resultstore.Action_Id{
   869  									InvocationId: "uuid-222",
   870  									TargetId:     "tgt-uuid-2",
   871  									ActionId:     "walking",
   872  								},
   873  							},
   874  						}, {
   875  							TargetProto: &resultstore.Target{
   876  								Name: "/testgrid/backend:tabulator",
   877  								Id: &resultstore.Target_Id{
   878  									InvocationId: "uuid-222",
   879  									TargetId:     "tgt-uuid-2",
   880  								},
   881  							},
   882  							ConfiguredTargetProto: &resultstore.ConfiguredTarget{
   883  								Name: "/testgrid/backend:tabulator",
   884  								Id: &resultstore.ConfiguredTarget_Id{
   885  									InvocationId: "uuid-222",
   886  									TargetId:     "tgt-uuid-2",
   887  								},
   888  							},
   889  							ActionProto: &resultstore.Action{
   890  								Name: "/testgrid/backend:tabulator",
   891  								Id: &resultstore.Action_Id{
   892  									InvocationId: "uuid-222",
   893  									TargetId:     "tgt-uuid-2",
   894  									ActionId:     "flying",
   895  								},
   896  							},
   897  						},
   898  					},
   899  				},
   900  			},
   901  		},
   902  	}
   903  
   904  	for _, tc := range cases {
   905  		t.Run(tc.name, func(t *testing.T) {
   906  			got := processRawResult(logrus.WithField("case", tc.name), tc.result)
   907  			if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" {
   908  				t.Errorf("processRawResult(...) differed (-want, +got): %s", diff)
   909  			}
   910  		})
   911  	}
   912  
   913  }
   914  func TestProcessGroup(t *testing.T) {
   915  
   916  	cases := []struct {
   917  		name  string
   918  		tg    *configpb.TestGroup
   919  		group *invocationGroup
   920  		want  *updater.InflatedColumn
   921  	}{
   922  		{
   923  			name: "nil",
   924  			want: nil,
   925  		},
   926  		{
   927  			name:  "empty",
   928  			group: &invocationGroup{},
   929  			want:  nil,
   930  		},
   931  		{
   932  			name: "basic invocation group",
   933  			group: &invocationGroup{
   934  				GroupID: "uuid-123",
   935  				Invocations: []*invocation{
   936  					{
   937  						InvocationProto: &resultstore.Invocation{
   938  							Name: invocationName("uuid-123"),
   939  							Id: &resultstore.Invocation_Id{
   940  								InvocationId: "uuid-123",
   941  							},
   942  							Timing: &resultstore.Timing{
   943  								StartTime: &timestamppb.Timestamp{
   944  									Seconds: 1234,
   945  								},
   946  							},
   947  						},
   948  						TargetResults: map[string][]*singleActionResult{
   949  							"tgt-id-1": {
   950  								{
   951  									ConfiguredTargetProto: &resultstore.ConfiguredTarget{
   952  										Id: &resultstore.ConfiguredTarget_Id{
   953  											TargetId: "tgt-id-1",
   954  										},
   955  										StatusAttributes: &resultstore.StatusAttributes{
   956  											Status: resultstore.Status_PASSED,
   957  										},
   958  									},
   959  								},
   960  							},
   961  							"tgt-id-2": {
   962  								{
   963  									ConfiguredTargetProto: &resultstore.ConfiguredTarget{
   964  										Id: &resultstore.ConfiguredTarget_Id{
   965  											TargetId: "tgt-id-2",
   966  										},
   967  										StatusAttributes: &resultstore.StatusAttributes{
   968  											Status: resultstore.Status_FAILED,
   969  										},
   970  									},
   971  								},
   972  							},
   973  						},
   974  					},
   975  				},
   976  			},
   977  			want: &updater.InflatedColumn{
   978  				Column: &statepb.Column{
   979  					Name:    "uuid-123",
   980  					Build:   "uuid-123",
   981  					Started: 1234000,
   982  					Hint:    "1970-01-01T00:20:34Z",
   983  				},
   984  				Cells: map[string]updater.Cell{
   985  					"tgt-id-1": {
   986  						ID:     "tgt-id-1",
   987  						CellID: "uuid-123",
   988  						Result: teststatuspb.TestStatus_PASS,
   989  					},
   990  					"tgt-id-2": {
   991  						ID:     "tgt-id-2",
   992  						CellID: "uuid-123",
   993  						Result: teststatuspb.TestStatus_FAIL,
   994  					},
   995  				},
   996  			},
   997  		},
   998  		{
   999  			name: "advanced invocation group with several invocations and repeated targets",
  1000  			tg: &configpb.TestGroup{
  1001  				BuildOverrideConfigurationValue: "pi-key-chu",
  1002  			},
  1003  			group: &invocationGroup{
  1004  				GroupID: "snorlax",
  1005  				Invocations: []*invocation{
  1006  					{
  1007  						InvocationProto: &resultstore.Invocation{
  1008  							Name: invocationName("uuid-123"),
  1009  							Id: &resultstore.Invocation_Id{
  1010  								InvocationId: "uuid-123",
  1011  							},
  1012  							Timing: &resultstore.Timing{
  1013  								StartTime: &timestamppb.Timestamp{
  1014  									Seconds: 1234,
  1015  								},
  1016  							},
  1017  							Properties: []*resultstore.Property{
  1018  								{
  1019  									Key:   "pi-key-chu",
  1020  									Value: "snorlax",
  1021  								},
  1022  							},
  1023  						},
  1024  						TargetResults: map[string][]*singleActionResult{
  1025  							"tgt-id-1": {
  1026  								{
  1027  									ConfiguredTargetProto: &resultstore.ConfiguredTarget{
  1028  										Id: &resultstore.ConfiguredTarget_Id{
  1029  											TargetId: "tgt-id-1",
  1030  										},
  1031  										StatusAttributes: &resultstore.StatusAttributes{
  1032  											Status: resultstore.Status_PASSED,
  1033  										},
  1034  									},
  1035  								},
  1036  							},
  1037  							"tgt-id-2": {
  1038  								{
  1039  									ConfiguredTargetProto: &resultstore.ConfiguredTarget{
  1040  										Id: &resultstore.ConfiguredTarget_Id{
  1041  											TargetId: "tgt-id-2",
  1042  										},
  1043  										StatusAttributes: &resultstore.StatusAttributes{
  1044  											Status: resultstore.Status_FAILED,
  1045  										},
  1046  									},
  1047  								},
  1048  							},
  1049  						},
  1050  					},
  1051  					{
  1052  						InvocationProto: &resultstore.Invocation{
  1053  							Name: invocationName("uuid-124"),
  1054  							Id: &resultstore.Invocation_Id{
  1055  								InvocationId: "uuid-124",
  1056  							},
  1057  							Timing: &resultstore.Timing{
  1058  								StartTime: &timestamppb.Timestamp{
  1059  									Seconds: 1334,
  1060  								},
  1061  							},
  1062  							Properties: []*resultstore.Property{
  1063  								{
  1064  									Key:   "pi-key-chu",
  1065  									Value: "snorlax",
  1066  								},
  1067  							},
  1068  						},
  1069  						TargetResults: map[string][]*singleActionResult{
  1070  							"tgt-id-1": {
  1071  								{
  1072  									ConfiguredTargetProto: &resultstore.ConfiguredTarget{
  1073  										Id: &resultstore.ConfiguredTarget_Id{
  1074  											TargetId: "tgt-id-1",
  1075  										},
  1076  										StatusAttributes: &resultstore.StatusAttributes{
  1077  											Status: resultstore.Status_PASSED,
  1078  										},
  1079  									},
  1080  								},
  1081  							},
  1082  							"tgt-id-2": {
  1083  								{
  1084  									ConfiguredTargetProto: &resultstore.ConfiguredTarget{
  1085  										Id: &resultstore.ConfiguredTarget_Id{
  1086  											TargetId: "tgt-id-2",
  1087  										},
  1088  										StatusAttributes: &resultstore.StatusAttributes{
  1089  											Status: resultstore.Status_FAILED,
  1090  										},
  1091  									},
  1092  								},
  1093  							},
  1094  						},
  1095  					},
  1096  				},
  1097  			},
  1098  			want: &updater.InflatedColumn{
  1099  				Column: &statepb.Column{
  1100  					Name:    "snorlax",
  1101  					Build:   "snorlax",
  1102  					Started: 1234000,
  1103  					Hint:    "1970-01-01T00:22:14Z",
  1104  				},
  1105  				Cells: map[string]updater.Cell{
  1106  					"tgt-id-1": {
  1107  						ID:     "tgt-id-1",
  1108  						CellID: "uuid-123",
  1109  						Result: teststatuspb.TestStatus_PASS,
  1110  					},
  1111  					"tgt-id-2": {
  1112  						ID:     "tgt-id-2",
  1113  						CellID: "uuid-123",
  1114  						Result: teststatuspb.TestStatus_FAIL,
  1115  					},
  1116  					"tgt-id-1 [1]": {
  1117  						ID:     "tgt-id-1",
  1118  						CellID: "uuid-124",
  1119  						Result: teststatuspb.TestStatus_PASS,
  1120  					},
  1121  					"tgt-id-2 [1]": {
  1122  						ID:     "tgt-id-2",
  1123  						CellID: "uuid-124",
  1124  						Result: teststatuspb.TestStatus_FAIL,
  1125  					},
  1126  				},
  1127  			},
  1128  		},
  1129  		{
  1130  			name: "invocation group with single invocation and disabled test result",
  1131  			tg: &configpb.TestGroup{
  1132  				BuildOverrideConfigurationValue: "pi-key-chu",
  1133  				MaxTestMethodsPerTest:           10,
  1134  				EnableTestMethods:               true,
  1135  			},
  1136  			group: &invocationGroup{
  1137  				GroupID: "snorlax",
  1138  				Invocations: []*invocation{
  1139  					{
  1140  						InvocationProto: &resultstore.Invocation{
  1141  							Name: invocationName("uuid-123"),
  1142  							Id: &resultstore.Invocation_Id{
  1143  								InvocationId: "uuid-123",
  1144  							},
  1145  							Timing: &resultstore.Timing{
  1146  								StartTime: &timestamppb.Timestamp{
  1147  									Seconds: 1234,
  1148  								},
  1149  							},
  1150  							Properties: []*resultstore.Property{
  1151  								{
  1152  									Key:   "pi-key-chu",
  1153  									Value: "snorlax",
  1154  								},
  1155  							},
  1156  						},
  1157  						TargetResults: map[string][]*singleActionResult{
  1158  							"tgt-id-1": {
  1159  								{
  1160  									ConfiguredTargetProto: &resultstore.ConfiguredTarget{
  1161  										Id: &resultstore.ConfiguredTarget_Id{
  1162  											TargetId: "tgt-id-1",
  1163  										},
  1164  										StatusAttributes: &resultstore.StatusAttributes{
  1165  											Status: resultstore.Status_PASSED,
  1166  										},
  1167  									},
  1168  									ActionProto: &resultstore.Action{
  1169  										ActionType: &resultstore.Action_TestAction{
  1170  											TestAction: &resultstore.TestAction{
  1171  												TestSuite: &resultstore.TestSuite{
  1172  													SuiteName: "TestDetectJSError",
  1173  													Tests: []*resultstore.Test{
  1174  														{
  1175  															TestType: &resultstore.Test_TestCase{
  1176  																TestCase: &resultstore.TestCase{
  1177  																	CaseName: "DISABLED_case",
  1178  																	Result:   resultstore.TestCase_SKIPPED,
  1179  																},
  1180  															},
  1181  														},
  1182  													},
  1183  												},
  1184  											},
  1185  										},
  1186  									},
  1187  								},
  1188  							},
  1189  						},
  1190  					},
  1191  				},
  1192  			},
  1193  			want: &updater.InflatedColumn{
  1194  				Column: &statepb.Column{
  1195  					Name:    "snorlax",
  1196  					Build:   "snorlax",
  1197  					Started: 1234000,
  1198  					Hint:    "1970-01-01T00:20:34Z",
  1199  				},
  1200  				Cells: map[string]updater.Cell{
  1201  					"tgt-id-1": {
  1202  						ID:     "tgt-id-1",
  1203  						CellID: "uuid-123",
  1204  						Result: teststatuspb.TestStatus_PASS,
  1205  					},
  1206  					"tgt-id-1@TESTGRID@DISABLED_case": {
  1207  						Result: teststatuspb.TestStatus_PASS_WITH_SKIPS,
  1208  						ID:     "tgt-id-1",
  1209  						CellID: "uuid-123"},
  1210  				},
  1211  			},
  1212  		},
  1213  		{
  1214  			name: "invocation group with single invocation and test result with failure and error",
  1215  			tg: &configpb.TestGroup{
  1216  				BuildOverrideConfigurationValue: "pi-key-chu",
  1217  				MaxTestMethodsPerTest:           10,
  1218  				EnableTestMethods:               true,
  1219  			},
  1220  			group: &invocationGroup{
  1221  				GroupID: "snorlax",
  1222  				Invocations: []*invocation{
  1223  					{
  1224  						InvocationProto: &resultstore.Invocation{
  1225  							Name: invocationName("uuid-123"),
  1226  							Id: &resultstore.Invocation_Id{
  1227  								InvocationId: "uuid-123",
  1228  							},
  1229  							Timing: &resultstore.Timing{
  1230  								StartTime: &timestamppb.Timestamp{
  1231  									Seconds: 1234,
  1232  								},
  1233  							},
  1234  							Properties: []*resultstore.Property{
  1235  								{
  1236  									Key:   "pi-key-chu",
  1237  									Value: "snorlax",
  1238  								},
  1239  							},
  1240  						},
  1241  						TargetResults: map[string][]*singleActionResult{
  1242  							"tgt-id-1": {
  1243  								{
  1244  									ConfiguredTargetProto: &resultstore.ConfiguredTarget{
  1245  										Id: &resultstore.ConfiguredTarget_Id{
  1246  											TargetId: "tgt-id-1",
  1247  										},
  1248  										StatusAttributes: &resultstore.StatusAttributes{
  1249  											Status: resultstore.Status_PASSED,
  1250  										},
  1251  									},
  1252  									ActionProto: &resultstore.Action{
  1253  										ActionType: &resultstore.Action_TestAction{
  1254  											TestAction: &resultstore.TestAction{
  1255  												TestSuite: &resultstore.TestSuite{
  1256  													SuiteName: "TestDetectJSError",
  1257  													Tests: []*resultstore.Test{
  1258  														{
  1259  															TestType: &resultstore.Test_TestCase{
  1260  																TestCase: &resultstore.TestCase{
  1261  																	CaseName: "Not_working_case",
  1262  																	Failures: []*resultstore.TestFailure{
  1263  																		{
  1264  																			FailureMessage: "foo",
  1265  																		},
  1266  																	},
  1267  																	Errors: []*resultstore.TestError{
  1268  																		{
  1269  																			ErrorMessage: "bar",
  1270  																		},
  1271  																	},
  1272  																},
  1273  															},
  1274  														},
  1275  													},
  1276  												},
  1277  											},
  1278  										},
  1279  									},
  1280  								},
  1281  							},
  1282  						},
  1283  					},
  1284  				},
  1285  			},
  1286  			want: &updater.InflatedColumn{
  1287  				Column: &statepb.Column{
  1288  					Name:    "snorlax",
  1289  					Build:   "snorlax",
  1290  					Started: 1234000,
  1291  					Hint:    "1970-01-01T00:20:34Z",
  1292  				},
  1293  				Cells: map[string]updater.Cell{
  1294  					"tgt-id-1": {
  1295  						ID:     "tgt-id-1",
  1296  						CellID: "uuid-123",
  1297  						Result: teststatuspb.TestStatus_PASS,
  1298  					},
  1299  					"tgt-id-1@TESTGRID@Not_working_case": {
  1300  						Result: teststatuspb.TestStatus_FAIL,
  1301  						ID:     "tgt-id-1",
  1302  						CellID: "uuid-123"},
  1303  				},
  1304  			},
  1305  		},
  1306  		{
  1307  			name: "invocation group with ignored statuses and custom target status evaluator",
  1308  			tg: &configpb.TestGroup{
  1309  				IgnorePending: true,
  1310  				CustomEvaluatorRuleSet: &evalpb.RuleSet{
  1311  					Rules: []*evalpb.Rule{
  1312  						{
  1313  							ComputedStatus: teststatuspb.TestStatus_CATEGORIZED_ABORT,
  1314  							TestResultComparisons: []*evalpb.TestResultComparison{
  1315  								{
  1316  									TestResultInfo: &evalpb.TestResultComparison_TargetStatus{
  1317  										TargetStatus: true,
  1318  									},
  1319  									Comparison: &evalpb.Comparison{
  1320  										Op: evalpb.Comparison_OP_EQ,
  1321  										ComparisonValue: &evalpb.Comparison_TargetStatusValue{
  1322  											TargetStatusValue: teststatuspb.TestStatus_TIMED_OUT,
  1323  										},
  1324  									},
  1325  								},
  1326  							},
  1327  						},
  1328  					},
  1329  				},
  1330  			},
  1331  			group: &invocationGroup{
  1332  				GroupID: "uuid-123",
  1333  				Invocations: []*invocation{
  1334  					{
  1335  						InvocationProto: &resultstore.Invocation{
  1336  							Name: invocationName("uuid-123"),
  1337  							Id: &resultstore.Invocation_Id{
  1338  								InvocationId: "uuid-123",
  1339  							},
  1340  							Timing: &resultstore.Timing{
  1341  								StartTime: &timestamppb.Timestamp{
  1342  									Seconds: 1234,
  1343  								},
  1344  							},
  1345  						},
  1346  						TargetResults: map[string][]*singleActionResult{
  1347  							"tgt-id-1": {
  1348  								{
  1349  									ConfiguredTargetProto: &resultstore.ConfiguredTarget{
  1350  										Id: &resultstore.ConfiguredTarget_Id{
  1351  											TargetId: "tgt-id-1",
  1352  										},
  1353  										StatusAttributes: &resultstore.StatusAttributes{
  1354  											Status: resultstore.Status_PASSED,
  1355  										},
  1356  									},
  1357  								},
  1358  							},
  1359  							"tgt-id-2": {
  1360  								{
  1361  									ConfiguredTargetProto: &resultstore.ConfiguredTarget{
  1362  										Id: &resultstore.ConfiguredTarget_Id{
  1363  											TargetId: "tgt-id-2",
  1364  										},
  1365  										StatusAttributes: &resultstore.StatusAttributes{
  1366  											Status: resultstore.Status_TESTING,
  1367  										},
  1368  									},
  1369  								},
  1370  							},
  1371  							"tgt-id-3": {
  1372  								{
  1373  									ConfiguredTargetProto: &resultstore.ConfiguredTarget{
  1374  										Id: &resultstore.ConfiguredTarget_Id{
  1375  											TargetId: "tgt-id-3",
  1376  										},
  1377  										StatusAttributes: &resultstore.StatusAttributes{
  1378  											Status: resultstore.Status_TIMED_OUT,
  1379  										},
  1380  									},
  1381  								},
  1382  							},
  1383  						},
  1384  					},
  1385  				},
  1386  			},
  1387  			want: &updater.InflatedColumn{
  1388  				Column: &statepb.Column{
  1389  					Name:    "uuid-123",
  1390  					Build:   "uuid-123",
  1391  					Started: 1234000,
  1392  					Hint:    "1970-01-01T00:20:34Z",
  1393  				},
  1394  				Cells: map[string]updater.Cell{
  1395  					"tgt-id-1": {
  1396  						ID:     "tgt-id-1",
  1397  						CellID: "uuid-123",
  1398  						Result: teststatuspb.TestStatus_PASS,
  1399  					},
  1400  					"tgt-id-3": {
  1401  						ID:     "tgt-id-3",
  1402  						CellID: "uuid-123",
  1403  						Result: teststatuspb.TestStatus_CATEGORIZED_ABORT,
  1404  					},
  1405  				},
  1406  			},
  1407  		},
  1408  	}
  1409  	for _, tc := range cases {
  1410  		t.Run(tc.name, func(t *testing.T) {
  1411  			got := processGroup(tc.tg, tc.group)
  1412  			if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" {
  1413  				t.Errorf("processGroup() differed (-want, +got): %s", diff)
  1414  			}
  1415  		})
  1416  	}
  1417  }
  1418  
  1419  func TestFilterResults(t *testing.T) {
  1420  	cases := []struct {
  1421  		name       string
  1422  		results    []*resultstore.Test
  1423  		properties []*configpb.TestGroup_KeyValue
  1424  		match      *string
  1425  		unmatch    *string
  1426  		expected   []*resultstore.Test
  1427  		filtered   bool
  1428  	}{
  1429  		{
  1430  			name: "basically works",
  1431  			results: []*resultstore.Test{
  1432  				{
  1433  					TestType: &resultstore.Test_TestCase{
  1434  						TestCase: &resultstore.TestCase{
  1435  							CaseName: "every",
  1436  						},
  1437  					},
  1438  				},
  1439  				{
  1440  					TestType: &resultstore.Test_TestCase{
  1441  						TestCase: &resultstore.TestCase{
  1442  							CaseName: "thing",
  1443  						},
  1444  					},
  1445  				},
  1446  			},
  1447  			expected: []*resultstore.Test{
  1448  				{
  1449  					TestType: &resultstore.Test_TestCase{
  1450  						TestCase: &resultstore.TestCase{
  1451  							CaseName: "every",
  1452  						},
  1453  					},
  1454  				},
  1455  				{
  1456  					TestType: &resultstore.Test_TestCase{
  1457  						TestCase: &resultstore.TestCase{
  1458  							CaseName: "thing",
  1459  						},
  1460  					},
  1461  				},
  1462  			},
  1463  		},
  1464  		{
  1465  			name: "match nothing",
  1466  			results: []*resultstore.Test{
  1467  				{
  1468  					TestType: &resultstore.Test_TestCase{
  1469  						TestCase: &resultstore.TestCase{
  1470  							CaseName: "every",
  1471  						},
  1472  					},
  1473  				},
  1474  				{
  1475  					TestType: &resultstore.Test_TestCase{
  1476  						TestCase: &resultstore.TestCase{
  1477  							CaseName: "thing",
  1478  						},
  1479  					},
  1480  				},
  1481  			},
  1482  			match:    pstr("sandwiches"),
  1483  			filtered: true,
  1484  		},
  1485  		{
  1486  			name: "all wrong properties",
  1487  			results: []*resultstore.Test{
  1488  				{
  1489  					TestType: &resultstore.Test_TestCase{
  1490  						TestCase: &resultstore.TestCase{
  1491  							CaseName: "every",
  1492  						},
  1493  					},
  1494  				},
  1495  				{
  1496  					TestType: &resultstore.Test_TestCase{
  1497  						TestCase: &resultstore.TestCase{
  1498  							CaseName: "thing",
  1499  						},
  1500  					},
  1501  				},
  1502  			},
  1503  			properties: []*configpb.TestGroup_KeyValue{
  1504  				{
  1505  					Key:   "medal",
  1506  					Value: "gold",
  1507  				},
  1508  			},
  1509  			filtered: true,
  1510  		},
  1511  		{
  1512  			name:       "properties flter",
  1513  			properties: []*configpb.TestGroup_KeyValue{{}},
  1514  			filtered:   true,
  1515  		},
  1516  		{
  1517  			name:     "match filters",
  1518  			match:    pstr(".*"),
  1519  			filtered: true,
  1520  		},
  1521  		{
  1522  			name:     "unmatch filters",
  1523  			match:    pstr("^$"),
  1524  			filtered: true,
  1525  		},
  1526  		{
  1527  			name:    "match fruit",
  1528  			match:   pstr("tomato|apple|orange"),
  1529  			unmatch: pstr("tomato"),
  1530  			results: []*resultstore.Test{
  1531  				{
  1532  					TestType: &resultstore.Test_TestCase{
  1533  						TestCase: &resultstore.TestCase{
  1534  							CaseName: "steak",
  1535  						},
  1536  					},
  1537  				},
  1538  				{
  1539  					TestType: &resultstore.Test_TestCase{
  1540  						TestCase: &resultstore.TestCase{
  1541  							CaseName: "tomato",
  1542  						},
  1543  					},
  1544  				},
  1545  				{
  1546  					TestType: &resultstore.Test_TestCase{
  1547  						TestCase: &resultstore.TestCase{
  1548  							CaseName: "apple",
  1549  						},
  1550  					},
  1551  				},
  1552  				{
  1553  					TestType: &resultstore.Test_TestCase{
  1554  						TestCase: &resultstore.TestCase{
  1555  							CaseName: "orange",
  1556  						},
  1557  					},
  1558  				},
  1559  			},
  1560  			expected: []*resultstore.Test{
  1561  				{
  1562  					TestType: &resultstore.Test_TestCase{
  1563  						TestCase: &resultstore.TestCase{
  1564  							CaseName: "apple",
  1565  						},
  1566  					},
  1567  				},
  1568  				{
  1569  					TestType: &resultstore.Test_TestCase{
  1570  						TestCase: &resultstore.TestCase{
  1571  							CaseName: "orange",
  1572  						},
  1573  					},
  1574  				},
  1575  			},
  1576  			filtered: true,
  1577  		},
  1578  		{
  1579  			name: "good properties",
  1580  			properties: []*configpb.TestGroup_KeyValue{
  1581  				{
  1582  					Key:   "tastes",
  1583  					Value: "good",
  1584  				},
  1585  			},
  1586  			results: []*resultstore.Test{
  1587  				{
  1588  					TestType: &resultstore.Test_TestCase{
  1589  						TestCase: &resultstore.TestCase{
  1590  							CaseName: "potion",
  1591  							Properties: []*resultstore.Property{
  1592  								{
  1593  									Key:   "tastes",
  1594  									Value: "bad",
  1595  								},
  1596  							},
  1597  						},
  1598  					},
  1599  				},
  1600  				{
  1601  					TestType: &resultstore.Test_TestCase{
  1602  						TestCase: &resultstore.TestCase{
  1603  							CaseName: "fruit",
  1604  							Properties: []*resultstore.Property{
  1605  								{
  1606  									Key:   "tastes",
  1607  									Value: "good",
  1608  								},
  1609  							},
  1610  						},
  1611  					},
  1612  				},
  1613  			},
  1614  			expected: []*resultstore.Test{
  1615  				{
  1616  					TestType: &resultstore.Test_TestCase{
  1617  						TestCase: &resultstore.TestCase{
  1618  							CaseName: "fruit",
  1619  							Properties: []*resultstore.Property{
  1620  								{
  1621  									Key:   "tastes",
  1622  									Value: "good",
  1623  								},
  1624  							},
  1625  						},
  1626  					},
  1627  				},
  1628  			},
  1629  			filtered: true,
  1630  		},
  1631  		{
  1632  			name: "both filter",
  1633  			properties: []*configpb.TestGroup_KeyValue{
  1634  				{
  1635  					Key:   "tastes",
  1636  					Value: "good",
  1637  				},
  1638  			},
  1639  			unmatch: pstr("steak"),
  1640  			results: []*resultstore.Test{
  1641  				{
  1642  					TestType: &resultstore.Test_TestCase{
  1643  						TestCase: &resultstore.TestCase{
  1644  							CaseName: "potion",
  1645  							Properties: []*resultstore.Property{
  1646  								{
  1647  									Key:   "tastes",
  1648  									Value: "bad",
  1649  								},
  1650  							},
  1651  						},
  1652  					},
  1653  				},
  1654  				{
  1655  					TestType: &resultstore.Test_TestCase{
  1656  						TestCase: &resultstore.TestCase{
  1657  							CaseName: "fruit",
  1658  							Properties: []*resultstore.Property{
  1659  								{
  1660  									Key:   "tastes",
  1661  									Value: "good",
  1662  								},
  1663  							},
  1664  						},
  1665  					},
  1666  				},
  1667  				{
  1668  					TestType: &resultstore.Test_TestCase{
  1669  						TestCase: &resultstore.TestCase{
  1670  							CaseName: "steak",
  1671  							Properties: []*resultstore.Property{
  1672  								{
  1673  									Key:   "tastes",
  1674  									Value: "good",
  1675  								},
  1676  							},
  1677  						},
  1678  					},
  1679  				},
  1680  			},
  1681  			expected: []*resultstore.Test{
  1682  				{
  1683  					TestType: &resultstore.Test_TestCase{
  1684  						TestCase: &resultstore.TestCase{
  1685  							CaseName: "fruit",
  1686  							Properties: []*resultstore.Property{
  1687  								{
  1688  									Key:   "tastes",
  1689  									Value: "good",
  1690  								},
  1691  							},
  1692  						},
  1693  					},
  1694  				},
  1695  			},
  1696  			filtered: true,
  1697  		},
  1698  	}
  1699  
  1700  	for _, tc := range cases {
  1701  		t.Run(tc.name, func(t *testing.T) {
  1702  			var match, unmatch *regexp.Regexp
  1703  			if tc.match != nil {
  1704  				match = regexp.MustCompile(*tc.match)
  1705  			}
  1706  			if tc.unmatch != nil {
  1707  				unmatch = regexp.MustCompile(*tc.unmatch)
  1708  			}
  1709  			actual, filtered := filterResults(tc.results, tc.properties, match, unmatch)
  1710  			if diff := cmp.Diff(actual, tc.expected, protocmp.Transform()); diff != "" {
  1711  				t.Errorf("filterResults() got unexpected diff (-have, +want):\n%s", diff)
  1712  			}
  1713  			if filtered != tc.filtered {
  1714  				t.Errorf("filterResults() got filtered %t, want %t", filtered, tc.filtered)
  1715  			}
  1716  		})
  1717  	}
  1718  }
  1719  
  1720  func TestFilterProperties(t *testing.T) {
  1721  	cases := []struct {
  1722  		name       string
  1723  		results    []*resultstore.Test
  1724  		properties []*configpb.TestGroup_KeyValue
  1725  		expected   []*resultstore.Test
  1726  	}{
  1727  		{
  1728  			name: "basically works",
  1729  			results: []*resultstore.Test{
  1730  				{
  1731  					TestType: &resultstore.Test_TestCase{
  1732  						TestCase: &resultstore.TestCase{
  1733  							CaseName: "every",
  1734  						},
  1735  					},
  1736  				},
  1737  				{
  1738  					TestType: &resultstore.Test_TestCase{
  1739  						TestCase: &resultstore.TestCase{
  1740  							CaseName: "every",
  1741  						},
  1742  					},
  1743  				},
  1744  			},
  1745  			expected: []*resultstore.Test{
  1746  				{
  1747  					TestType: &resultstore.Test_TestCase{
  1748  						TestCase: &resultstore.TestCase{
  1749  							CaseName: "every",
  1750  						},
  1751  					},
  1752  				},
  1753  				{
  1754  					TestType: &resultstore.Test_TestCase{
  1755  						TestCase: &resultstore.TestCase{
  1756  							CaseName: "every",
  1757  						},
  1758  					},
  1759  				},
  1760  			},
  1761  		},
  1762  		{
  1763  			name: "must have correct key and value",
  1764  			properties: []*configpb.TestGroup_KeyValue{
  1765  				{
  1766  					Key:   "goal",
  1767  					Value: "gold",
  1768  				},
  1769  			},
  1770  			results: []*resultstore.Test{
  1771  				{
  1772  					TestType: &resultstore.Test_TestCase{
  1773  						TestCase: &resultstore.TestCase{
  1774  							CaseName: "wrong-key",
  1775  							Properties: []*resultstore.Property{
  1776  								{
  1777  									Key:   "random",
  1778  									Value: "gold",
  1779  								},
  1780  							},
  1781  						},
  1782  					},
  1783  				},
  1784  				{
  1785  					TestType: &resultstore.Test_TestCase{
  1786  						TestCase: &resultstore.TestCase{
  1787  							CaseName: "correct-key",
  1788  							Properties: []*resultstore.Property{
  1789  								{
  1790  									Key:   "goal",
  1791  									Value: "gold",
  1792  								},
  1793  							},
  1794  						},
  1795  					},
  1796  				},
  1797  				{
  1798  					TestType: &resultstore.Test_TestCase{
  1799  						TestCase: &resultstore.TestCase{
  1800  							CaseName: "wrong-value",
  1801  							Properties: []*resultstore.Property{
  1802  								{
  1803  									Key:   "goal",
  1804  									Value: "silver",
  1805  								},
  1806  							},
  1807  						},
  1808  					},
  1809  				},
  1810  				{
  1811  					TestType: &resultstore.Test_TestCase{
  1812  						TestCase: &resultstore.TestCase{
  1813  							CaseName: "multiple-key-value-pairs",
  1814  							Properties: []*resultstore.Property{
  1815  								{
  1816  									Key:   "silver",
  1817  									Value: "medal",
  1818  								},
  1819  								{
  1820  									Key:   "goal",
  1821  									Value: "gold",
  1822  								},
  1823  								{
  1824  									Key:   "critical",
  1825  									Value: "information",
  1826  								},
  1827  							},
  1828  						},
  1829  					},
  1830  				},
  1831  			},
  1832  			expected: []*resultstore.Test{
  1833  				{
  1834  					TestType: &resultstore.Test_TestCase{
  1835  						TestCase: &resultstore.TestCase{
  1836  							CaseName: "correct-key",
  1837  							Properties: []*resultstore.Property{
  1838  								{
  1839  									Key:   "goal",
  1840  									Value: "gold",
  1841  								},
  1842  							},
  1843  						},
  1844  					},
  1845  				},
  1846  				{
  1847  					TestType: &resultstore.Test_TestCase{
  1848  						TestCase: &resultstore.TestCase{
  1849  							CaseName: "multiple-key-value-pairs",
  1850  							Properties: []*resultstore.Property{
  1851  								{
  1852  									Key:   "silver",
  1853  									Value: "medal",
  1854  								},
  1855  								{
  1856  									Key:   "goal",
  1857  									Value: "gold",
  1858  								},
  1859  								{
  1860  									Key:   "critical",
  1861  									Value: "information",
  1862  								},
  1863  							},
  1864  						},
  1865  					},
  1866  				},
  1867  			},
  1868  		},
  1869  		{
  1870  			name: "must match all properties",
  1871  			properties: []*configpb.TestGroup_KeyValue{
  1872  				{
  1873  					Key:   "medal",
  1874  					Value: "gold",
  1875  				},
  1876  				{
  1877  					Key:   "ribbon",
  1878  					Value: "blue",
  1879  				},
  1880  			},
  1881  			results: []*resultstore.Test{
  1882  				{
  1883  					TestType: &resultstore.Test_TestCase{
  1884  						TestCase: &resultstore.TestCase{
  1885  							CaseName: "zero",
  1886  						},
  1887  					},
  1888  				},
  1889  				{
  1890  					TestType: &resultstore.Test_TestCase{
  1891  						TestCase: &resultstore.TestCase{
  1892  							CaseName: "wrong-medal",
  1893  							Properties: []*resultstore.Property{
  1894  								{
  1895  									Key:   "ribbon",
  1896  									Value: "blue",
  1897  								},
  1898  							},
  1899  						},
  1900  					},
  1901  				},
  1902  				{
  1903  					TestType: &resultstore.Test_TestCase{
  1904  						TestCase: &resultstore.TestCase{
  1905  							CaseName: "wrong-ribbon",
  1906  							Properties: []*resultstore.Property{
  1907  								{
  1908  									Key:   "medal",
  1909  									Value: "gold",
  1910  								},
  1911  							},
  1912  						},
  1913  					},
  1914  				},
  1915  				{
  1916  					TestType: &resultstore.Test_TestCase{
  1917  						TestCase: &resultstore.TestCase{
  1918  							CaseName: "whole-deal",
  1919  							Properties: []*resultstore.Property{
  1920  								{
  1921  									Key:   "medal",
  1922  									Value: "gold",
  1923  								},
  1924  								{
  1925  									Key:   "ribbon",
  1926  									Value: "blue",
  1927  								},
  1928  							},
  1929  						},
  1930  					},
  1931  				},
  1932  			},
  1933  			expected: []*resultstore.Test{
  1934  				{
  1935  					TestType: &resultstore.Test_TestCase{
  1936  						TestCase: &resultstore.TestCase{
  1937  							CaseName: "whole-deal",
  1938  							Properties: []*resultstore.Property{
  1939  								{
  1940  									Key:   "medal",
  1941  									Value: "gold",
  1942  								},
  1943  								{
  1944  									Key:   "ribbon",
  1945  									Value: "blue",
  1946  								},
  1947  							},
  1948  						},
  1949  					},
  1950  				},
  1951  			},
  1952  		},
  1953  	}
  1954  
  1955  	for _, tc := range cases {
  1956  		t.Run(tc.name, func(t *testing.T) {
  1957  			actual := filterProperties(tc.results, tc.properties)
  1958  			if diff := cmp.Diff(actual, tc.expected, protocmp.Transform()); diff != "" {
  1959  				t.Errorf("filterProperties() got unexpected diff (-have, +want):\n%s", diff)
  1960  			}
  1961  		})
  1962  	}
  1963  }
  1964  
  1965  func TestFillProperties(t *testing.T) {
  1966  	cases := []struct {
  1967  		name       string
  1968  		properties map[string]string
  1969  		result     *resultstore.Test
  1970  		match      map[string]bool
  1971  		want       map[string]string
  1972  	}{
  1973  		{
  1974  			name: "basically works",
  1975  		},
  1976  		{
  1977  			name:   "still basically works",
  1978  			result: &resultstore.Test{},
  1979  		},
  1980  		{
  1981  			name: "simple case no match",
  1982  			properties: map[string]string{
  1983  				"spongebob": "yellow",
  1984  				"patrick":   "pink",
  1985  			},
  1986  			result: &resultstore.Test{
  1987  				TestType: &resultstore.Test_TestCase{
  1988  					TestCase: &resultstore.TestCase{
  1989  						Properties: []*resultstore.Property{
  1990  							{Key: "squidward", Value: "green"},
  1991  						},
  1992  					},
  1993  				},
  1994  			},
  1995  			want: map[string]string{
  1996  				"spongebob": "yellow",
  1997  				"patrick":   "pink",
  1998  				"squidward": "green",
  1999  			},
  2000  		},
  2001  		{
  2002  			name: "simple case with match",
  2003  			properties: map[string]string{
  2004  				"spongebob": "yellow",
  2005  				"mrkrabs":   "red",
  2006  			},
  2007  			result: &resultstore.Test{
  2008  				TestType: &resultstore.Test_TestCase{
  2009  					TestCase: &resultstore.TestCase{
  2010  						Properties: []*resultstore.Property{
  2011  							{Key: "squidward", Value: "blue"},
  2012  						},
  2013  					},
  2014  				},
  2015  			},
  2016  			match: map[string]bool{
  2017  				"squidward": true,
  2018  			},
  2019  			want: map[string]string{
  2020  				"spongebob": "yellow",
  2021  				"mrkrabs":   "red",
  2022  				"squidward": "blue",
  2023  			},
  2024  		},
  2025  	}
  2026  	for _, tc := range cases {
  2027  		t.Run(tc.name, func(t *testing.T) {
  2028  			fillProperties(tc.properties, tc.result, tc.match)
  2029  			if diff := cmp.Diff(tc.want, tc.properties, protocmp.Transform()); diff != "" {
  2030  				t.Errorf("fillProperties() differed (-want, +got): [%s]", diff)
  2031  			}
  2032  		})
  2033  	}
  2034  }
  2035  
  2036  func pstr(s string) *string { return &s }
  2037  
  2038  func TestMatchResults(t *testing.T) {
  2039  	cases := []struct {
  2040  		name     string
  2041  		results  []*resultstore.Test
  2042  		match    *string
  2043  		unmatch  *string
  2044  		expected []*resultstore.Test
  2045  	}{
  2046  		{
  2047  			name: "basically works",
  2048  			results: []*resultstore.Test{
  2049  				{
  2050  					TestType: &resultstore.Test_TestCase{
  2051  						TestCase: &resultstore.TestCase{
  2052  							CaseName: "every",
  2053  						},
  2054  					},
  2055  				},
  2056  				{
  2057  					TestType: &resultstore.Test_TestCase{
  2058  						TestCase: &resultstore.TestCase{
  2059  							CaseName: "thing",
  2060  						},
  2061  					},
  2062  				},
  2063  			},
  2064  			expected: []*resultstore.Test{
  2065  				{
  2066  					TestType: &resultstore.Test_TestCase{
  2067  						TestCase: &resultstore.TestCase{
  2068  							CaseName: "every",
  2069  						},
  2070  					},
  2071  				},
  2072  				{
  2073  					TestType: &resultstore.Test_TestCase{
  2074  						TestCase: &resultstore.TestCase{
  2075  							CaseName: "thing",
  2076  						},
  2077  					},
  2078  				},
  2079  			},
  2080  		},
  2081  		{
  2082  			name: "match results",
  2083  			results: []*resultstore.Test{
  2084  				{
  2085  					TestType: &resultstore.Test_TestCase{
  2086  						TestCase: &resultstore.TestCase{
  2087  							CaseName: "miss",
  2088  						},
  2089  					},
  2090  				},
  2091  				{
  2092  					TestType: &resultstore.Test_TestCase{
  2093  						TestCase: &resultstore.TestCase{
  2094  							CaseName: "yesgopher",
  2095  						},
  2096  					},
  2097  				},
  2098  				{
  2099  					TestType: &resultstore.Test_TestCase{
  2100  						TestCase: &resultstore.TestCase{
  2101  							CaseName: "gopher-yes",
  2102  						},
  2103  					},
  2104  				},
  2105  				{
  2106  					TestType: &resultstore.Test_TestCase{
  2107  						TestCase: &resultstore.TestCase{
  2108  							CaseName: "no",
  2109  						},
  2110  					},
  2111  				},
  2112  			},
  2113  			match: pstr("gopher"),
  2114  			expected: []*resultstore.Test{
  2115  				{
  2116  					TestType: &resultstore.Test_TestCase{
  2117  						TestCase: &resultstore.TestCase{
  2118  							CaseName: "yesgopher",
  2119  						},
  2120  					},
  2121  				},
  2122  				{
  2123  					TestType: &resultstore.Test_TestCase{
  2124  						TestCase: &resultstore.TestCase{
  2125  							CaseName: "gopher-yes",
  2126  						},
  2127  					},
  2128  				},
  2129  			},
  2130  		},
  2131  		{
  2132  			name: "exclude results with neutral alignments",
  2133  			results: []*resultstore.Test{
  2134  				{
  2135  					TestType: &resultstore.Test_TestCase{
  2136  						TestCase: &resultstore.TestCase{
  2137  							CaseName: "yesgopher",
  2138  						},
  2139  					},
  2140  				},
  2141  				{
  2142  					TestType: &resultstore.Test_TestCase{
  2143  						TestCase: &resultstore.TestCase{
  2144  							CaseName: "lawful good",
  2145  						},
  2146  					},
  2147  				},
  2148  				{
  2149  					TestType: &resultstore.Test_TestCase{
  2150  						TestCase: &resultstore.TestCase{
  2151  							CaseName: "neutral good",
  2152  						},
  2153  					},
  2154  				},
  2155  				{
  2156  					TestType: &resultstore.Test_TestCase{
  2157  						TestCase: &resultstore.TestCase{
  2158  							CaseName: "chaotic good",
  2159  						},
  2160  					},
  2161  				},
  2162  				{
  2163  					TestType: &resultstore.Test_TestCase{
  2164  						TestCase: &resultstore.TestCase{
  2165  							CaseName: "lawful neutral",
  2166  						},
  2167  					},
  2168  				},
  2169  				{
  2170  					TestType: &resultstore.Test_TestCase{
  2171  						TestCase: &resultstore.TestCase{
  2172  							CaseName: "true neutral",
  2173  						},
  2174  					},
  2175  				},
  2176  				{
  2177  					TestType: &resultstore.Test_TestCase{
  2178  						TestCase: &resultstore.TestCase{
  2179  							CaseName: "chaotic neutral",
  2180  						},
  2181  					},
  2182  				},
  2183  				{
  2184  					TestType: &resultstore.Test_TestCase{
  2185  						TestCase: &resultstore.TestCase{
  2186  							CaseName: "lawful evil",
  2187  						},
  2188  					},
  2189  				},
  2190  				{
  2191  					TestType: &resultstore.Test_TestCase{
  2192  						TestCase: &resultstore.TestCase{
  2193  							CaseName: "neutral evil",
  2194  						},
  2195  					},
  2196  				},
  2197  				{
  2198  					TestType: &resultstore.Test_TestCase{
  2199  						TestCase: &resultstore.TestCase{
  2200  							CaseName: "chaotic evil",
  2201  						},
  2202  					},
  2203  				},
  2204  			},
  2205  			unmatch: pstr("neutral"),
  2206  			expected: []*resultstore.Test{
  2207  				{
  2208  					TestType: &resultstore.Test_TestCase{
  2209  						TestCase: &resultstore.TestCase{
  2210  							CaseName: "yesgopher",
  2211  						},
  2212  					},
  2213  				},
  2214  				{
  2215  					TestType: &resultstore.Test_TestCase{
  2216  						TestCase: &resultstore.TestCase{
  2217  							CaseName: "lawful good",
  2218  						},
  2219  					},
  2220  				},
  2221  				{
  2222  					TestType: &resultstore.Test_TestCase{
  2223  						TestCase: &resultstore.TestCase{
  2224  							CaseName: "chaotic good",
  2225  						},
  2226  					},
  2227  				},
  2228  				{
  2229  					TestType: &resultstore.Test_TestCase{
  2230  						TestCase: &resultstore.TestCase{
  2231  							CaseName: "lawful evil",
  2232  						},
  2233  					},
  2234  				},
  2235  				{
  2236  					TestType: &resultstore.Test_TestCase{
  2237  						TestCase: &resultstore.TestCase{
  2238  							CaseName: "chaotic evil",
  2239  						},
  2240  					},
  2241  				},
  2242  			},
  2243  		},
  2244  		{
  2245  			name: "exclude the included results",
  2246  			results: []*resultstore.Test{
  2247  				{
  2248  					TestType: &resultstore.Test_TestCase{
  2249  						TestCase: &resultstore.TestCase{
  2250  							CaseName: "lawful good",
  2251  						},
  2252  					},
  2253  				},
  2254  				{
  2255  					TestType: &resultstore.Test_TestCase{
  2256  						TestCase: &resultstore.TestCase{
  2257  							CaseName: "neutral good",
  2258  						},
  2259  					},
  2260  				},
  2261  				{
  2262  					TestType: &resultstore.Test_TestCase{
  2263  						TestCase: &resultstore.TestCase{
  2264  							CaseName: "chaotic good",
  2265  						},
  2266  					},
  2267  				},
  2268  				{
  2269  					TestType: &resultstore.Test_TestCase{
  2270  						TestCase: &resultstore.TestCase{
  2271  							CaseName: "lawful neutral",
  2272  						},
  2273  					},
  2274  				},
  2275  				{
  2276  					TestType: &resultstore.Test_TestCase{
  2277  						TestCase: &resultstore.TestCase{
  2278  							CaseName: "true neutral",
  2279  						},
  2280  					},
  2281  				},
  2282  				{
  2283  					TestType: &resultstore.Test_TestCase{
  2284  						TestCase: &resultstore.TestCase{
  2285  							CaseName: "chaotic neutral",
  2286  						},
  2287  					},
  2288  				},
  2289  				{
  2290  					TestType: &resultstore.Test_TestCase{
  2291  						TestCase: &resultstore.TestCase{
  2292  							CaseName: "lawful evil",
  2293  						},
  2294  					},
  2295  				},
  2296  				{
  2297  					TestType: &resultstore.Test_TestCase{
  2298  						TestCase: &resultstore.TestCase{
  2299  							CaseName: "neutral evil",
  2300  						},
  2301  					},
  2302  				},
  2303  				{
  2304  					TestType: &resultstore.Test_TestCase{
  2305  						TestCase: &resultstore.TestCase{
  2306  							CaseName: "chaotic evil",
  2307  						},
  2308  					},
  2309  				},
  2310  			},
  2311  			match:   pstr("good"),
  2312  			unmatch: pstr("neutral"),
  2313  			expected: []*resultstore.Test{
  2314  				{
  2315  					TestType: &resultstore.Test_TestCase{
  2316  						TestCase: &resultstore.TestCase{
  2317  							CaseName: "lawful good",
  2318  						},
  2319  					},
  2320  				},
  2321  				{
  2322  					TestType: &resultstore.Test_TestCase{
  2323  						TestCase: &resultstore.TestCase{
  2324  							CaseName: "chaotic good",
  2325  						},
  2326  					},
  2327  				},
  2328  			},
  2329  		},
  2330  	}
  2331  
  2332  	for _, tc := range cases {
  2333  		t.Run(tc.name, func(t *testing.T) {
  2334  			var match, unmatch *regexp.Regexp
  2335  			if tc.match != nil {
  2336  				match = regexp.MustCompile(*tc.match)
  2337  			}
  2338  			if tc.unmatch != nil {
  2339  				unmatch = regexp.MustCompile(*tc.unmatch)
  2340  			}
  2341  			actual := matchResults(tc.results, match, unmatch)
  2342  			for i, r := range actual {
  2343  				if diff := cmp.Diff(r, tc.expected[i], protocmp.Transform()); diff != "" {
  2344  					t.Errorf("matchResults() got unexpected diff (-have, +want):\n%s", diff)
  2345  				}
  2346  			}
  2347  
  2348  		})
  2349  	}
  2350  }
  2351  
  2352  func TestGetTestResults(t *testing.T) {
  2353  	cases := []struct {
  2354  		name      string
  2355  		testsuite *resultstore.TestSuite
  2356  		want      []*resultstore.Test
  2357  	}{
  2358  		{
  2359  			name: "empty test suite",
  2360  			testsuite: &resultstore.TestSuite{
  2361  				SuiteName: "Empty Test",
  2362  			},
  2363  			want: []*resultstore.Test{
  2364  				{
  2365  					TestType: &resultstore.Test_TestSuite{
  2366  						TestSuite: &resultstore.TestSuite{
  2367  							SuiteName: "Empty Test",
  2368  						},
  2369  					},
  2370  				},
  2371  			},
  2372  		},
  2373  		{
  2374  			name:      "nil test suite",
  2375  			testsuite: nil,
  2376  			want:      nil,
  2377  		},
  2378  		{
  2379  			name: "standard test suite",
  2380  			testsuite: &resultstore.TestSuite{
  2381  				SuiteName: "Standard test suite",
  2382  				Tests: []*resultstore.Test{
  2383  					{
  2384  						TestType: &resultstore.Test_TestSuite{
  2385  							TestSuite: &resultstore.TestSuite{
  2386  								SuiteName: "TestDetectJSError",
  2387  								Tests: []*resultstore.Test{
  2388  									{
  2389  										TestType: &resultstore.Test_TestCase{
  2390  											TestCase: &resultstore.TestCase{
  2391  												CaseName: "TestDetectJSError/Main",
  2392  											},
  2393  										},
  2394  									},
  2395  									{
  2396  										TestType: &resultstore.Test_TestCase{
  2397  											TestCase: &resultstore.TestCase{
  2398  												CaseName: "TestDetectJSError/Summary",
  2399  											},
  2400  										},
  2401  									},
  2402  									{
  2403  										TestType: &resultstore.Test_TestCase{
  2404  											TestCase: &resultstore.TestCase{
  2405  												CaseName: "TestDetectJSError/Dashboard",
  2406  											},
  2407  										},
  2408  									},
  2409  								},
  2410  							},
  2411  						},
  2412  					},
  2413  				},
  2414  			},
  2415  			want: []*resultstore.Test{
  2416  				{
  2417  					TestType: &resultstore.Test_TestCase{
  2418  						TestCase: &resultstore.TestCase{
  2419  							CaseName: "TestDetectJSError/Main",
  2420  						},
  2421  					},
  2422  				},
  2423  				{
  2424  					TestType: &resultstore.Test_TestCase{
  2425  						TestCase: &resultstore.TestCase{
  2426  							CaseName: "TestDetectJSError/Summary",
  2427  						},
  2428  					},
  2429  				},
  2430  				{
  2431  					TestType: &resultstore.Test_TestCase{
  2432  						TestCase: &resultstore.TestCase{
  2433  							CaseName: "TestDetectJSError/Dashboard",
  2434  						},
  2435  					},
  2436  				},
  2437  			},
  2438  		},
  2439  		{
  2440  			name: "nested test suite",
  2441  			testsuite: &resultstore.TestSuite{
  2442  				SuiteName: "Nested test suite",
  2443  				Tests: []*resultstore.Test{
  2444  					{
  2445  						TestType: &resultstore.Test_TestSuite{
  2446  							TestSuite: &resultstore.TestSuite{
  2447  								SuiteName: "TestDetectJSError",
  2448  								Tests: []*resultstore.Test{
  2449  									{
  2450  										TestType: &resultstore.Test_TestCase{
  2451  											TestCase: &resultstore.TestCase{
  2452  												CaseName: "TestDetectJSError/Main",
  2453  											},
  2454  										},
  2455  									},
  2456  									{
  2457  										TestType: &resultstore.Test_TestCase{
  2458  											TestCase: &resultstore.TestCase{
  2459  												CaseName: "TestDetectJSError/Summary",
  2460  											},
  2461  										},
  2462  									},
  2463  									{
  2464  										TestType: &resultstore.Test_TestSuite{
  2465  											TestSuite: &resultstore.TestSuite{
  2466  												SuiteName: "TestDetectJSError/Other",
  2467  												Tests: []*resultstore.Test{
  2468  													{
  2469  														TestType: &resultstore.Test_TestCase{
  2470  															TestCase: &resultstore.TestCase{
  2471  																CaseName: "TestDetectJSError/Misc",
  2472  															},
  2473  														},
  2474  													},
  2475  												},
  2476  											},
  2477  										},
  2478  									},
  2479  								},
  2480  							},
  2481  						},
  2482  					},
  2483  				},
  2484  			},
  2485  			want: []*resultstore.Test{
  2486  				{
  2487  					TestType: &resultstore.Test_TestCase{
  2488  						TestCase: &resultstore.TestCase{
  2489  							CaseName: "TestDetectJSError/Main",
  2490  						},
  2491  					},
  2492  				},
  2493  				{
  2494  					TestType: &resultstore.Test_TestCase{
  2495  						TestCase: &resultstore.TestCase{
  2496  							CaseName: "TestDetectJSError/Summary",
  2497  						},
  2498  					},
  2499  				},
  2500  				{
  2501  					TestType: &resultstore.Test_TestCase{
  2502  						TestCase: &resultstore.TestCase{
  2503  							CaseName: "TestDetectJSError/Misc",
  2504  						},
  2505  					},
  2506  				},
  2507  			},
  2508  		},
  2509  	}
  2510  	for _, tc := range cases {
  2511  		t.Run(tc.name, func(t *testing.T) {
  2512  			got := getTestResults(tc.testsuite)
  2513  			if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" {
  2514  				t.Errorf("getTestResults(%+v) differed (-want, +got): [%s]", tc.testsuite, diff)
  2515  			}
  2516  		})
  2517  	}
  2518  }
  2519  
  2520  func TestMethodRegex(t *testing.T) {
  2521  	regexErrYes := regexp.MustCompile("yes")
  2522  	regexErrNo := regexp.MustCompile("no")
  2523  	type result struct {
  2524  		matchMethods      *regexp.Regexp
  2525  		unmatchMethods    *regexp.Regexp
  2526  		matchMethodsErr   error
  2527  		unmatchMethodsErr error
  2528  	}
  2529  	cases := []struct {
  2530  		name string
  2531  		tg   *configpb.TestGroup
  2532  		want result
  2533  	}{
  2534  		{
  2535  			name: "basically works",
  2536  			tg:   &configpb.TestGroup{},
  2537  		},
  2538  		{
  2539  			name: "basic test",
  2540  			tg: &configpb.TestGroup{
  2541  				TestMethodMatchRegex:   "yes",
  2542  				TestMethodUnmatchRegex: "no",
  2543  			},
  2544  			want: result{
  2545  				matchMethods:      regexErrYes,
  2546  				unmatchMethods:    regexErrNo,
  2547  				matchMethodsErr:   nil,
  2548  				unmatchMethodsErr: nil,
  2549  			},
  2550  		},
  2551  		{
  2552  			name: "invalid regex test",
  2553  			tg: &configpb.TestGroup{
  2554  				TestMethodMatchRegex:   "\x8A",
  2555  				TestMethodUnmatchRegex: "\x8A",
  2556  			},
  2557  			want: result{
  2558  				matchMethods:      nil,
  2559  				unmatchMethods:    nil,
  2560  				matchMethodsErr:   nil,
  2561  				unmatchMethodsErr: nil,
  2562  			},
  2563  		},
  2564  	}
  2565  	for _, tc := range cases {
  2566  		t.Run(tc.name, func(t *testing.T) {
  2567  			mm, umm, mmErr, ummErr := testMethodRegex(tc.tg)
  2568  			res := result{
  2569  				matchMethods:      mm,
  2570  				unmatchMethods:    umm,
  2571  				matchMethodsErr:   mmErr,
  2572  				unmatchMethodsErr: ummErr,
  2573  			}
  2574  			if diff := cmp.Diff(tc.want.matchMethods, res.matchMethods, protocmp.Transform(), cmp.AllowUnexported(regexp.Regexp{})); diff != "" {
  2575  				t.Errorf("testMethodRegex(%+v) differed (-want, +got): [%s]", tc.tg, diff)
  2576  			}
  2577  			if diff := cmp.Diff(tc.want.unmatchMethods, res.unmatchMethods, protocmp.Transform(), cmp.AllowUnexported(regexp.Regexp{})); diff != "" {
  2578  				t.Errorf("testMethodRegex(%+v) differed (-want, +got): [%s]", tc.tg, diff)
  2579  			}
  2580  		})
  2581  	}
  2582  }
  2583  
  2584  func TestQueryAfter(t *testing.T) {
  2585  	now := time.Now()
  2586  	cases := []struct {
  2587  		name  string
  2588  		query string
  2589  		when  time.Time
  2590  		want  string
  2591  	}{
  2592  		{
  2593  			name: "empty",
  2594  			want: "",
  2595  		},
  2596  		{
  2597  			name:  "zero",
  2598  			query: prowLabel,
  2599  			when:  time.Time{},
  2600  			want:  "invocation_attributes.labels:\"prow\" timing.start_time>=\"0001-01-01T00:00:00Z\"",
  2601  		},
  2602  		{
  2603  			name:  "basic",
  2604  			query: prowLabel,
  2605  			when:  now,
  2606  			want:  fmt.Sprintf("invocation_attributes.labels:\"prow\" timing.start_time>=\"%s\"", now.UTC().Format(time.RFC3339)),
  2607  		},
  2608  	}
  2609  	for _, tc := range cases {
  2610  		t.Run(tc.name, func(t *testing.T) {
  2611  			got := queryAfter(tc.query, tc.when)
  2612  			if diff := cmp.Diff(tc.want, got); diff != "" {
  2613  				t.Errorf("queryAfter(%q, %v) differed (-want, +got): %s", tc.query, tc.when, diff)
  2614  			}
  2615  		})
  2616  	}
  2617  }
  2618  
  2619  func TestQueryProw(t *testing.T) {
  2620  	now := time.Now()
  2621  	cases := []struct {
  2622  		name      string
  2623  		query     string
  2624  		when      time.Time
  2625  		want      string
  2626  		wantError bool
  2627  	}{
  2628  		{
  2629  			name: "empty",
  2630  			want: "invocation_attributes.labels:\"prow\" timing.start_time>=\"0001-01-01T00:00:00Z\"",
  2631  		},
  2632  		{
  2633  			name:  "zero",
  2634  			query: "",
  2635  			when:  time.Time{},
  2636  			want:  "invocation_attributes.labels:\"prow\" timing.start_time>=\"0001-01-01T00:00:00Z\"",
  2637  		},
  2638  		{
  2639  			name:  "basic",
  2640  			query: "",
  2641  			when:  now,
  2642  			want:  fmt.Sprintf("invocation_attributes.labels:\"prow\" timing.start_time>=\"%s\"", now.UTC().Format(time.RFC3339)),
  2643  		},
  2644  		{
  2645  			name:  "target",
  2646  			query: `target:"my-job"`,
  2647  			when:  now,
  2648  			want:  fmt.Sprintf("id.target_id=\"my-job\" invocation.invocation_attributes.labels:\"prow\" timing.start_time>=\"%s\"", now.UTC().Format(time.RFC3339)),
  2649  		},
  2650  		{
  2651  			name:      "invalid query",
  2652  			query:     `label:foo bar`,
  2653  			when:      now,
  2654  			wantError: true,
  2655  		},
  2656  	}
  2657  	for _, tc := range cases {
  2658  		t.Run(tc.name, func(t *testing.T) {
  2659  			got, err := queryProw(tc.query, tc.when)
  2660  			if !tc.wantError && err != nil {
  2661  				t.Errorf("queryProw(%q, %v) errored: %v", tc.query, tc.when, err)
  2662  			} else if tc.wantError && err == nil {
  2663  				t.Errorf("queryProw(%q, %v) did not error as expected", tc.query, tc.when)
  2664  			}
  2665  			if diff := cmp.Diff(tc.want, got); diff != "" {
  2666  				t.Errorf("queryProw(%q, %v) differed (-want, +got): %s", tc.query, tc.when, diff)
  2667  			}
  2668  		})
  2669  	}
  2670  }
  2671  
  2672  func TestSearch(t *testing.T) {
  2673  	twoDaysAgo := time.Now().AddDate(0, 0, -2)
  2674  	testQueryAfter := queryAfter(prowLabel, twoDaysAgo)
  2675  	testTargetQueryAfter, _ := queryProw(`target:"my-job"`, twoDaysAgo)
  2676  	cases := []struct {
  2677  		name     string
  2678  		stop     time.Time
  2679  		client   *fakeClient
  2680  		rsConfig *configpb.ResultStoreConfig
  2681  		want     []string
  2682  		wantErr  bool
  2683  	}{
  2684  		{
  2685  			name:    "nil",
  2686  			wantErr: true,
  2687  		},
  2688  		{
  2689  			name:     "empty",
  2690  			rsConfig: &configpb.ResultStoreConfig{},
  2691  			client:   &fakeClient{},
  2692  			wantErr:  true,
  2693  		},
  2694  		{
  2695  			name: "no results",
  2696  			rsConfig: &configpb.ResultStoreConfig{
  2697  				Project: "my-project",
  2698  			},
  2699  			client:  &fakeClient{},
  2700  			wantErr: true,
  2701  		},
  2702  		{
  2703  			name: "basic",
  2704  			rsConfig: &configpb.ResultStoreConfig{
  2705  				Project: "my-project",
  2706  			},
  2707  			client: &fakeClient{
  2708  				searches: map[string][]string{
  2709  					testQueryAfter: {"id-1", "id-2", "id-3"},
  2710  				},
  2711  			},
  2712  			stop: twoDaysAgo,
  2713  			want: []string{"id-1", "id-2", "id-3"},
  2714  		},
  2715  		{
  2716  			name: "target",
  2717  			rsConfig: &configpb.ResultStoreConfig{
  2718  				Project: "my-project",
  2719  				Query:   `target:"my-job"`,
  2720  			},
  2721  			client: &fakeClient{
  2722  				searches: map[string][]string{
  2723  					testQueryAfter:       {"id-1", "id-2", "id-3"},
  2724  					testTargetQueryAfter: {"id-1", "id-3"},
  2725  				},
  2726  			},
  2727  			stop: twoDaysAgo,
  2728  			want: []string{"id-1", "id-3"},
  2729  		},
  2730  		{
  2731  			name: "invalid query",
  2732  			rsConfig: &configpb.ResultStoreConfig{
  2733  				Project: "my-project",
  2734  				Query:   "label:foo bar",
  2735  			},
  2736  			client: &fakeClient{
  2737  				searches: map[string][]string{
  2738  					testQueryAfter:       {"id-1", "id-2", "id-3"},
  2739  					testTargetQueryAfter: {"id-1", "id-3"},
  2740  				},
  2741  			},
  2742  			stop:    twoDaysAgo,
  2743  			wantErr: true,
  2744  		},
  2745  	}
  2746  	for _, tc := range cases {
  2747  		t.Run(tc.name, func(t *testing.T) {
  2748  			var dlClient *DownloadClient
  2749  			if tc.client != nil {
  2750  				dlClient = &DownloadClient{client: tc.client}
  2751  			}
  2752  			got, err := search(context.Background(), logrus.WithField("case", tc.name), dlClient, tc.rsConfig, tc.stop)
  2753  			if err != nil && !tc.wantErr {
  2754  				t.Errorf("search() errored: %v", err)
  2755  			} else if err == nil && tc.wantErr {
  2756  				t.Errorf("search() did not error as expected")
  2757  			}
  2758  			if diff := cmp.Diff(tc.want, got); diff != "" {
  2759  				t.Errorf("search() differed (-want, +got): %s", diff)
  2760  			}
  2761  		})
  2762  	}
  2763  }
  2764  
  2765  func TestMostRecent(t *testing.T) {
  2766  	now := time.Now()
  2767  	oneHourAgo := now.Add(-1 * time.Hour)
  2768  	sixHoursAgo := now.Add(-6 * time.Hour)
  2769  	cases := []struct {
  2770  		name  string
  2771  		times []time.Time
  2772  		want  time.Time
  2773  	}{
  2774  		{
  2775  			name: "empty",
  2776  			want: time.Time{},
  2777  		},
  2778  		{
  2779  			name:  "single",
  2780  			times: []time.Time{oneHourAgo},
  2781  			want:  oneHourAgo,
  2782  		},
  2783  		{
  2784  			name:  "mix",
  2785  			times: []time.Time{now, oneHourAgo, sixHoursAgo},
  2786  			want:  now,
  2787  		},
  2788  	}
  2789  	for _, tc := range cases {
  2790  		t.Run(tc.name, func(t *testing.T) {
  2791  			got := mostRecent(tc.times)
  2792  			if !tc.want.Equal(got) {
  2793  				t.Errorf("stopFromColumns() differed; got %v, want %v", got, tc.want)
  2794  			}
  2795  		})
  2796  	}
  2797  }
  2798  
  2799  func TestStopFromColumns(t *testing.T) {
  2800  	now := time.Now()
  2801  	oneHourAgo := now.Add(-1 * time.Hour)
  2802  	sixHoursAgo := now.Add(-6 * time.Hour)
  2803  	b, _ := oneHourAgo.MarshalText()
  2804  	oneHourHint := string(b)
  2805  	cases := []struct {
  2806  		name string
  2807  		cols []updater.InflatedColumn
  2808  		want time.Time
  2809  	}{
  2810  		{
  2811  			name: "empty",
  2812  			want: time.Time{},
  2813  		},
  2814  		{
  2815  			name: "column start",
  2816  			cols: []updater.InflatedColumn{
  2817  				{
  2818  					Column: &statepb.Column{
  2819  						Started: float64(oneHourAgo.Unix() * 1000),
  2820  					},
  2821  				},
  2822  			},
  2823  			want: oneHourAgo.Truncate(time.Second),
  2824  		},
  2825  		{
  2826  			name: "column hint",
  2827  			cols: []updater.InflatedColumn{
  2828  				{
  2829  					Column: &statepb.Column{
  2830  						Started: float64(sixHoursAgo.Unix() * 1000),
  2831  						Hint:    oneHourHint,
  2832  					},
  2833  				},
  2834  			},
  2835  			want: oneHourAgo.Truncate(time.Second),
  2836  		},
  2837  	}
  2838  	for _, tc := range cases {
  2839  		t.Run(tc.name, func(t *testing.T) {
  2840  			got := stopFromColumns(logrus.WithField("case", tc.name), tc.cols)
  2841  			if !tc.want.Equal(got) {
  2842  				t.Errorf("stopFromColumns() differed; got %v, want %v", got, tc.want)
  2843  			}
  2844  		})
  2845  	}
  2846  }
  2847  
  2848  func TestUpdateStop(t *testing.T) {
  2849  	now := time.Now()
  2850  	oneHourAgo := now.Add(-1 * time.Hour)
  2851  	sixHoursAgo := now.Add(-6 * time.Hour)
  2852  	twoDaysAgo := now.AddDate(0, 0, -2)
  2853  	twoWeeksAgo := now.AddDate(0, 0, -14)
  2854  	oneMonthAgo := now.AddDate(0, 0, -30)
  2855  	b, _ := oneHourAgo.MarshalText()
  2856  	oneHourHint := string(b)
  2857  	cases := []struct {
  2858  		name        string
  2859  		tg          *configpb.TestGroup
  2860  		cols        []updater.InflatedColumn
  2861  		defaultStop time.Time
  2862  		reprocess   time.Duration
  2863  		want        time.Time
  2864  	}{
  2865  		{
  2866  			name: "empty",
  2867  			want: twoDaysAgo.Truncate(time.Second),
  2868  		},
  2869  		{
  2870  			name:      "reprocess",
  2871  			reprocess: 14 * 24 * time.Hour,
  2872  			want:      twoWeeksAgo.Truncate(time.Second),
  2873  		},
  2874  		{
  2875  			name: "days of results",
  2876  			tg: &configpb.TestGroup{
  2877  				DaysOfResults: 7,
  2878  			},
  2879  			want: twoWeeksAgo.Truncate(time.Second),
  2880  		},
  2881  		{
  2882  			name:        "default stop, no days of results",
  2883  			defaultStop: oneMonthAgo,
  2884  			want:        twoDaysAgo.Truncate(time.Second),
  2885  		},
  2886  		{
  2887  			name: "default stop earlier than days of results",
  2888  			tg: &configpb.TestGroup{
  2889  				DaysOfResults: 7,
  2890  			},
  2891  			defaultStop: oneMonthAgo,
  2892  			want:        twoWeeksAgo.Truncate(time.Second),
  2893  		},
  2894  		{
  2895  			name: "default stop later than days of results",
  2896  			tg: &configpb.TestGroup{
  2897  				DaysOfResults: 30,
  2898  			},
  2899  			defaultStop: twoWeeksAgo,
  2900  			want:        twoWeeksAgo.Truncate(time.Second),
  2901  		},
  2902  		{
  2903  			name: "column start",
  2904  			cols: []updater.InflatedColumn{
  2905  				{
  2906  					Column: &statepb.Column{
  2907  						Started: float64(oneHourAgo.Unix() * 1000),
  2908  					},
  2909  				},
  2910  			},
  2911  			defaultStop: twoWeeksAgo,
  2912  			want:        oneHourAgo.Truncate(time.Second),
  2913  		},
  2914  		{
  2915  			name: "column hint",
  2916  			cols: []updater.InflatedColumn{
  2917  				{
  2918  					Column: &statepb.Column{
  2919  						Started: float64(sixHoursAgo.Unix() * 1000),
  2920  						Hint:    oneHourHint,
  2921  					},
  2922  				},
  2923  			},
  2924  			defaultStop: twoWeeksAgo,
  2925  			want:        oneHourAgo.Truncate(time.Second),
  2926  		},
  2927  	}
  2928  	for _, tc := range cases {
  2929  		t.Run(tc.name, func(t *testing.T) {
  2930  			got := updateStop(logrus.WithField("testcase", tc.name), tc.tg, now, tc.cols, tc.defaultStop, tc.reprocess)
  2931  			if !tc.want.Equal(got) {
  2932  				t.Errorf("updateStop() differed; got %v, want %v", got, tc.want)
  2933  			}
  2934  		})
  2935  	}
  2936  }
  2937  
  2938  func TestIdentifyBuild(t *testing.T) {
  2939  	cases := []struct {
  2940  		name   string
  2941  		result *invocation
  2942  		tg     *configpb.TestGroup
  2943  		want   string
  2944  	}{
  2945  		{
  2946  			name: "no override configurations",
  2947  			result: &invocation{
  2948  				InvocationProto: &resultstore.Invocation{
  2949  					Name: invocationName("id-123"),
  2950  					Id: &resultstore.Invocation_Id{
  2951  						InvocationId: "id-123",
  2952  					},
  2953  				},
  2954  			},
  2955  			want: "",
  2956  		},
  2957  		{
  2958  			name: "override by non-existent property key",
  2959  			result: &invocation{
  2960  				InvocationProto: &resultstore.Invocation{
  2961  					Name: invocationName("id-1234"),
  2962  					Id: &resultstore.Invocation_Id{
  2963  						InvocationId: "id-1234",
  2964  					},
  2965  					Properties: []*resultstore.Property{
  2966  						{Key: "Luigi", Value: "Peaches"},
  2967  						{Key: "Bowser", Value: "Pingui"},
  2968  					},
  2969  				},
  2970  			},
  2971  			tg: &configpb.TestGroup{
  2972  				BuildOverrideConfigurationValue: "Mario",
  2973  			},
  2974  			want: "",
  2975  		},
  2976  		{
  2977  			name: "override by existent property key",
  2978  			result: &invocation{
  2979  				InvocationProto: &resultstore.Invocation{
  2980  					Name: invocationName("id-1234"),
  2981  					Id: &resultstore.Invocation_Id{
  2982  						InvocationId: "id-1234",
  2983  					},
  2984  					Properties: []*resultstore.Property{
  2985  						{Key: "Luigi", Value: "Peaches"},
  2986  						{Key: "Bowser", Value: "Pingui"},
  2987  						{Key: "Waluigi", Value: "Wapeaches"},
  2988  					},
  2989  				},
  2990  			},
  2991  			tg: &configpb.TestGroup{
  2992  				BuildOverrideConfigurationValue: "Waluigi",
  2993  			},
  2994  			want: "Wapeaches",
  2995  		},
  2996  		{
  2997  			name: "override by build time strf",
  2998  			result: &invocation{
  2999  				InvocationProto: &resultstore.Invocation{
  3000  					Name: invocationName("id-1234"),
  3001  					Id: &resultstore.Invocation_Id{
  3002  						InvocationId: "id-1234",
  3003  					},
  3004  					Timing: &resultstore.Timing{
  3005  						StartTime: &timestamppb.Timestamp{
  3006  							Seconds: 1689881216,
  3007  							Nanos:   27847,
  3008  						},
  3009  					},
  3010  				},
  3011  			},
  3012  			tg: &configpb.TestGroup{
  3013  				BuildOverrideStrftime: "%Y-%m-%d-%H",
  3014  			},
  3015  			want: "2023-07-20-19",
  3016  		},
  3017  	}
  3018  
  3019  	for _, tc := range cases {
  3020  		t.Run(tc.name, func(t *testing.T) {
  3021  			got := identifyBuild(tc.tg, tc.result)
  3022  			if diff := cmp.Diff(tc.want, got); diff != "" {
  3023  				t.Errorf("queryAfter(...) differed (-want, +got): %s", diff)
  3024  			}
  3025  		})
  3026  	}
  3027  }
  3028  
  3029  func TestIncludeStatus(t *testing.T) {
  3030  	cases := []struct {
  3031  		name string
  3032  		tg   *configpb.TestGroup
  3033  		sar  *singleActionResult
  3034  		want bool
  3035  	}{
  3036  		{
  3037  			name: "unspecifies status - not included",
  3038  			sar: &singleActionResult{
  3039  				ConfiguredTargetProto: &resultstore.ConfiguredTarget{
  3040  					StatusAttributes: &resultstore.StatusAttributes{
  3041  						Status: resultstore.Status_STATUS_UNSPECIFIED,
  3042  					},
  3043  				},
  3044  			},
  3045  			want: false,
  3046  		},
  3047  		{
  3048  			name: "built status and ignored - not included",
  3049  			tg: &configpb.TestGroup{
  3050  				IgnoreBuilt: true,
  3051  			},
  3052  			sar: &singleActionResult{
  3053  				ConfiguredTargetProto: &resultstore.ConfiguredTarget{
  3054  					StatusAttributes: &resultstore.StatusAttributes{
  3055  						Status: resultstore.Status_BUILT,
  3056  					},
  3057  				},
  3058  			},
  3059  			want: false,
  3060  		},
  3061  		{
  3062  			name: "built status and not ignored - included",
  3063  			tg: &configpb.TestGroup{
  3064  				IgnoreSkip: true,
  3065  			},
  3066  			sar: &singleActionResult{
  3067  				ConfiguredTargetProto: &resultstore.ConfiguredTarget{
  3068  					StatusAttributes: &resultstore.StatusAttributes{
  3069  						Status: resultstore.Status_BUILT,
  3070  					},
  3071  				},
  3072  			},
  3073  			want: true,
  3074  		},
  3075  		{
  3076  			name: "running status and ignored - not included",
  3077  			tg: &configpb.TestGroup{
  3078  				IgnorePending: true,
  3079  			},
  3080  			sar: &singleActionResult{
  3081  				ConfiguredTargetProto: &resultstore.ConfiguredTarget{
  3082  					StatusAttributes: &resultstore.StatusAttributes{
  3083  						Status: resultstore.Status_TESTING,
  3084  					},
  3085  				},
  3086  			},
  3087  			want: false,
  3088  		},
  3089  		{
  3090  			name: "running status and not ignored - included",
  3091  			tg: &configpb.TestGroup{
  3092  				IgnoreSkip: true,
  3093  			},
  3094  			sar: &singleActionResult{
  3095  				ConfiguredTargetProto: &resultstore.ConfiguredTarget{
  3096  					StatusAttributes: &resultstore.StatusAttributes{
  3097  						Status: resultstore.Status_TESTING,
  3098  					},
  3099  				},
  3100  			},
  3101  			want: true,
  3102  		},
  3103  		{
  3104  			name: "skipped status and ignored - not included",
  3105  			tg: &configpb.TestGroup{
  3106  				IgnoreSkip: true,
  3107  			},
  3108  			sar: &singleActionResult{
  3109  				ConfiguredTargetProto: &resultstore.ConfiguredTarget{
  3110  					StatusAttributes: &resultstore.StatusAttributes{
  3111  						Status: resultstore.Status_SKIPPED,
  3112  					},
  3113  				},
  3114  			},
  3115  			want: false,
  3116  		},
  3117  		{
  3118  			name: "other status - included",
  3119  			tg: &configpb.TestGroup{
  3120  				IgnoreSkip: true,
  3121  			},
  3122  			sar: &singleActionResult{
  3123  				ConfiguredTargetProto: &resultstore.ConfiguredTarget{
  3124  					StatusAttributes: &resultstore.StatusAttributes{
  3125  						Status: resultstore.Status_FAILED,
  3126  					},
  3127  				},
  3128  			},
  3129  			want: true,
  3130  		},
  3131  	}
  3132  
  3133  	for _, tc := range cases {
  3134  		t.Run(tc.name, func(t *testing.T) {
  3135  			got := includeStatus(tc.tg, tc.sar)
  3136  			if diff := cmp.Diff(tc.want, got); diff != "" {
  3137  				t.Errorf("includeStatus(...) differed (-want, +got): %s", diff)
  3138  			}
  3139  		})
  3140  	}
  3141  }
  3142  
  3143  func TestGroupInvocations(t *testing.T) {
  3144  	cases := []struct {
  3145  		name        string
  3146  		tg          *configpb.TestGroup
  3147  		invocations []*invocation
  3148  		want        []*invocationGroup
  3149  	}{
  3150  		{
  3151  			name: "grouping by build - build override by configuration value",
  3152  			tg: &configpb.TestGroup{
  3153  				PrimaryGrouping:                 configpb.TestGroup_PRIMARY_GROUPING_BUILD,
  3154  				BuildOverrideConfigurationValue: "my-property",
  3155  			},
  3156  			invocations: []*invocation{
  3157  				{
  3158  					InvocationProto: &resultstore.Invocation{
  3159  						Name: invocationName("uuid-123"),
  3160  						Id: &resultstore.Invocation_Id{
  3161  							InvocationId: "uuid-123",
  3162  						},
  3163  						Properties: []*resultstore.Property{
  3164  							{
  3165  								Key:   "my-property",
  3166  								Value: "my-value-1",
  3167  							},
  3168  						},
  3169  						Timing: &resultstore.Timing{
  3170  							StartTime: &timestamppb.Timestamp{
  3171  								Seconds: 1234,
  3172  							},
  3173  						},
  3174  					},
  3175  				},
  3176  				{
  3177  					InvocationProto: &resultstore.Invocation{
  3178  						Name: invocationName("uuid-321"),
  3179  						Id: &resultstore.Invocation_Id{
  3180  							InvocationId: "uuid-321",
  3181  						},
  3182  						Properties: []*resultstore.Property{
  3183  							{
  3184  								Key:   "my-property",
  3185  								Value: "my-value-2",
  3186  							},
  3187  						},
  3188  					},
  3189  				},
  3190  			},
  3191  			want: []*invocationGroup{
  3192  				{
  3193  					GroupID: "my-value-1",
  3194  					Invocations: []*invocation{
  3195  						{
  3196  							InvocationProto: &resultstore.Invocation{
  3197  								Name: invocationName("uuid-123"),
  3198  								Id: &resultstore.Invocation_Id{
  3199  									InvocationId: "uuid-123",
  3200  								},
  3201  								Properties: []*resultstore.Property{
  3202  									{
  3203  										Key:   "my-property",
  3204  										Value: "my-value-1",
  3205  									},
  3206  								},
  3207  								Timing: &resultstore.Timing{
  3208  									StartTime: &timestamppb.Timestamp{
  3209  										Seconds: 1234,
  3210  									},
  3211  								},
  3212  							},
  3213  						},
  3214  					},
  3215  				},
  3216  				{
  3217  					GroupID: "my-value-2",
  3218  					Invocations: []*invocation{
  3219  						{
  3220  							InvocationProto: &resultstore.Invocation{
  3221  								Name: invocationName("uuid-321"),
  3222  								Id: &resultstore.Invocation_Id{
  3223  									InvocationId: "uuid-321",
  3224  								},
  3225  								Properties: []*resultstore.Property{
  3226  									{
  3227  										Key:   "my-property",
  3228  										Value: "my-value-2",
  3229  									},
  3230  								},
  3231  							},
  3232  						},
  3233  					},
  3234  				},
  3235  			},
  3236  		},
  3237  		{
  3238  			name: "grouping by invocation id",
  3239  			invocations: []*invocation{
  3240  				{
  3241  					InvocationProto: &resultstore.Invocation{
  3242  						Name: invocationName("uuid-123"),
  3243  						Id: &resultstore.Invocation_Id{
  3244  							InvocationId: "uuid-123",
  3245  						},
  3246  						Timing: &resultstore.Timing{
  3247  							StartTime: &timestamppb.Timestamp{
  3248  								Seconds: 1234,
  3249  							},
  3250  						},
  3251  					},
  3252  				},
  3253  				{
  3254  					InvocationProto: &resultstore.Invocation{
  3255  						Name: invocationName("uuid-321"),
  3256  						Id: &resultstore.Invocation_Id{
  3257  							InvocationId: "uuid-321",
  3258  						},
  3259  					},
  3260  				},
  3261  			},
  3262  			want: []*invocationGroup{
  3263  				{
  3264  					GroupID: "uuid-123",
  3265  					Invocations: []*invocation{
  3266  						{
  3267  							InvocationProto: &resultstore.Invocation{
  3268  								Name: invocationName("uuid-123"),
  3269  								Id: &resultstore.Invocation_Id{
  3270  									InvocationId: "uuid-123",
  3271  								},
  3272  								Timing: &resultstore.Timing{
  3273  									StartTime: &timestamppb.Timestamp{
  3274  										Seconds: 1234,
  3275  									},
  3276  								},
  3277  							},
  3278  						},
  3279  					},
  3280  				},
  3281  				{
  3282  					GroupID: "uuid-321",
  3283  					Invocations: []*invocation{
  3284  						{
  3285  							InvocationProto: &resultstore.Invocation{
  3286  								Name: invocationName("uuid-321"),
  3287  								Id: &resultstore.Invocation_Id{
  3288  									InvocationId: "uuid-321",
  3289  								},
  3290  							},
  3291  						},
  3292  					},
  3293  				},
  3294  			},
  3295  		},
  3296  	}
  3297  
  3298  	for _, tc := range cases {
  3299  		t.Run(tc.name, func(t *testing.T) {
  3300  			got := groupInvocations(logrus.WithField("case", tc.name), tc.tg, tc.invocations)
  3301  			if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" {
  3302  				t.Errorf("groupInvocations(...) differed (-want, +got): %s", diff)
  3303  			}
  3304  		})
  3305  	}
  3306  }
  3307  
  3308  func TestExtractHeaders(t *testing.T) {
  3309  	cases := []struct {
  3310  		name       string
  3311  		isInv      bool
  3312  		inv        *invocation
  3313  		sar        *singleActionResult
  3314  		headerConf *configpb.TestGroup_ColumnHeader
  3315  		want       []string
  3316  	}{
  3317  		{
  3318  			name:  "empty invocation",
  3319  			isInv: true,
  3320  			want:  nil,
  3321  		},
  3322  		{
  3323  			name:  "empty target results",
  3324  			isInv: false,
  3325  			want:  nil,
  3326  		},
  3327  		{
  3328  			name:  "invocation has a config value present",
  3329  			isInv: true,
  3330  			inv: &invocation{
  3331  				InvocationProto: &resultstore.Invocation{
  3332  					Properties: []*resultstore.Property{
  3333  						{Key: "field", Value: "green"},
  3334  						{Key: "os", Value: "linux"},
  3335  					},
  3336  				},
  3337  			},
  3338  			headerConf: &configpb.TestGroup_ColumnHeader{
  3339  				ConfigurationValue: "os",
  3340  			},
  3341  			want: []string{"linux"},
  3342  		},
  3343  		{
  3344  			name:  "invocation doesn't have a config value present",
  3345  			isInv: true,
  3346  			inv: &invocation{
  3347  				InvocationProto: &resultstore.Invocation{
  3348  					Properties: []*resultstore.Property{
  3349  						{Key: "radio", Value: "head"},
  3350  					},
  3351  				},
  3352  			},
  3353  			headerConf: &configpb.TestGroup_ColumnHeader{
  3354  				ConfigurationValue: "rainbows",
  3355  			},
  3356  			want: nil,
  3357  		},
  3358  		{
  3359  			name:  "invocation has labels with prefixes",
  3360  			isInv: true,
  3361  			inv: &invocation{
  3362  				InvocationProto: &resultstore.Invocation{
  3363  					InvocationAttributes: &resultstore.InvocationAttributes{
  3364  						Labels: []string{"os=linux", "env=prod", "test=fast", "test=hermetic"},
  3365  					},
  3366  				},
  3367  			},
  3368  			headerConf: &configpb.TestGroup_ColumnHeader{
  3369  				Label: "test=",
  3370  			},
  3371  			want: []string{"fast", "hermetic"},
  3372  		},
  3373  		{
  3374  			name: "target result has existing properties",
  3375  			sar: &singleActionResult{
  3376  				ActionProto: &resultstore.Action{
  3377  					ActionType: &resultstore.Action_TestAction{
  3378  						TestAction: &resultstore.TestAction{
  3379  							TestSuite: &resultstore.TestSuite{
  3380  								Properties: []*resultstore.Property{
  3381  									{Key: "test-property", Value: "fast"},
  3382  								},
  3383  								Tests: []*resultstore.Test{
  3384  									{
  3385  										TestType: &resultstore.Test_TestCase{
  3386  											TestCase: &resultstore.TestCase{
  3387  												Properties: []*resultstore.Property{
  3388  													{Key: "test-property", Value: "hermetic"},
  3389  												},
  3390  											},
  3391  										},
  3392  									},
  3393  								},
  3394  							},
  3395  						},
  3396  					},
  3397  				},
  3398  			},
  3399  			headerConf: &configpb.TestGroup_ColumnHeader{
  3400  				Property: "test-property",
  3401  			},
  3402  			want: []string{"fast", "hermetic"},
  3403  		},
  3404  		{
  3405  			name: "target results doesn't have properties",
  3406  			sar: &singleActionResult{
  3407  				ActionProto: &resultstore.Action{},
  3408  			},
  3409  			want: nil,
  3410  		},
  3411  	}
  3412  
  3413  	for _, tc := range cases {
  3414  		t.Run(tc.name, func(t *testing.T) {
  3415  			var got []string
  3416  			switch {
  3417  			case tc.isInv:
  3418  				got = tc.inv.extractHeaders(tc.headerConf)
  3419  			default:
  3420  				got = tc.sar.extractHeaders(tc.headerConf)
  3421  			}
  3422  			if diff := cmp.Diff(tc.want, got, protocmp.Transform()); diff != "" {
  3423  				t.Errorf("extractHeaders(...) differed (-want, +got): %s", diff)
  3424  			}
  3425  		})
  3426  	}
  3427  }
  3428  
  3429  func TestCompileHeaders(t *testing.T) {
  3430  	cases := []struct {
  3431  		name          string
  3432  		columnHeaders []*configpb.TestGroup_ColumnHeader
  3433  		headers       [][]string
  3434  		want          []string
  3435  	}{
  3436  		{
  3437  			name: "no custom headers configured",
  3438  			want: nil,
  3439  		},
  3440  		{
  3441  			name: "single custom header set with no values fetched",
  3442  			columnHeaders: []*configpb.TestGroup_ColumnHeader{
  3443  				{Label: "rapid="},
  3444  			},
  3445  			headers: make([][]string, 1),
  3446  			want:    []string{""},
  3447  		},
  3448  		{
  3449  			name: "single custom header set with one value fetched",
  3450  			columnHeaders: []*configpb.TestGroup_ColumnHeader{
  3451  				{ConfigurationValue: "os"},
  3452  			},
  3453  			headers: [][]string{
  3454  				{"linux"},
  3455  			},
  3456  			want: []string{"linux"},
  3457  		},
  3458  		{
  3459  			name: "multiple custom headers set, don't list all",
  3460  			columnHeaders: []*configpb.TestGroup_ColumnHeader{
  3461  				{Label: "os="},
  3462  				{ConfigurationValue: "test-duration"},
  3463  			},
  3464  			headers: [][]string{
  3465  				{"linux", "ubuntu"},
  3466  				{"30m"},
  3467  			},
  3468  			want: []string{"*", "30m"},
  3469  		},
  3470  		{
  3471  			name: "multiple custom headers, list 'em all",
  3472  			columnHeaders: []*configpb.TestGroup_ColumnHeader{
  3473  				{Property: "type", ListAllValues: true},
  3474  				{Label: "test=", ListAllValues: true},
  3475  			},
  3476  			headers: [][]string{
  3477  				{"grass", "flying"},
  3478  				{"fast", "unit", "hermetic"},
  3479  			},
  3480  			want: []string{
  3481  				"flying||grass",
  3482  				"fast||hermetic||unit",
  3483  			},
  3484  		},
  3485  	}
  3486  
  3487  	for _, tc := range cases {
  3488  		t.Run(tc.name, func(t *testing.T) {
  3489  			got := compileHeaders(tc.columnHeaders, tc.headers)
  3490  			if diff := cmp.Diff(tc.want, got); diff != "" {
  3491  				t.Fatalf("compileHeaders(...) differed (-want,+got): %s", diff)
  3492  			}
  3493  		})
  3494  	}
  3495  }
  3496  
  3497  func TestCalculateMetrics(t *testing.T) {
  3498  	cases := []struct {
  3499  		name string
  3500  		sar  *singleActionResult
  3501  		want map[string]float64
  3502  	}{
  3503  		{
  3504  			name: "no numeric properties, no duration",
  3505  			sar: &singleActionResult{
  3506  				ActionProto: &resultstore.Action{
  3507  					ActionType: &resultstore.Action_TestAction{
  3508  						TestAction: &resultstore.TestAction{
  3509  							TestSuite: &resultstore.TestSuite{
  3510  								Properties: []*resultstore.Property{
  3511  									{Key: "marco", Value: "polo"},
  3512  								},
  3513  							},
  3514  						},
  3515  					},
  3516  				},
  3517  			},
  3518  			want: map[string]float64{},
  3519  		},
  3520  		{
  3521  			name: "no numeric properties, suite duration",
  3522  			sar: &singleActionResult{
  3523  				ActionProto: &resultstore.Action{
  3524  					ActionType: &resultstore.Action_TestAction{
  3525  						TestAction: &resultstore.TestAction{
  3526  							TestSuite: &resultstore.TestSuite{
  3527  								Properties: []*resultstore.Property{
  3528  									{Key: "marco", Value: "polo"},
  3529  								},
  3530  								Timing: &resultstore.Timing{
  3531  									Duration: &durationpb.Duration{
  3532  										Seconds: 120,
  3533  									},
  3534  								},
  3535  							},
  3536  						},
  3537  					},
  3538  				},
  3539  			},
  3540  			want: map[string]float64{
  3541  				updater.TestMethodsElapsedKey: 2,
  3542  			},
  3543  		},
  3544  		{
  3545  			name: "no numeric properties, cases duration only",
  3546  			sar: &singleActionResult{
  3547  				ActionProto: &resultstore.Action{
  3548  					ActionType: &resultstore.Action_TestAction{
  3549  						TestAction: &resultstore.TestAction{
  3550  							TestSuite: &resultstore.TestSuite{
  3551  								Properties: []*resultstore.Property{
  3552  									{Key: "marco", Value: "polo"},
  3553  								},
  3554  								Tests: []*resultstore.Test{
  3555  									{
  3556  										TestType: &resultstore.Test_TestCase{
  3557  											TestCase: &resultstore.TestCase{
  3558  												Timing: &resultstore.Timing{
  3559  													Duration: &durationpb.Duration{
  3560  														Seconds: 60,
  3561  													},
  3562  												},
  3563  											},
  3564  										},
  3565  									},
  3566  									{
  3567  										TestType: &resultstore.Test_TestCase{
  3568  											TestCase: &resultstore.TestCase{
  3569  												Timing: &resultstore.Timing{
  3570  													Duration: &durationpb.Duration{
  3571  														Seconds: 60,
  3572  													},
  3573  												},
  3574  											},
  3575  										},
  3576  									},
  3577  								},
  3578  							},
  3579  						},
  3580  					},
  3581  				},
  3582  			},
  3583  			want: map[string]float64{
  3584  				updater.TestMethodsElapsedKey: 2,
  3585  			},
  3586  		},
  3587  		{
  3588  			name: "numeric properties and durations",
  3589  			sar: &singleActionResult{
  3590  				ActionProto: &resultstore.Action{
  3591  					ActionType: &resultstore.Action_TestAction{
  3592  						TestAction: &resultstore.TestAction{
  3593  							TestSuite: &resultstore.TestSuite{
  3594  								Properties: []*resultstore.Property{
  3595  									{Key: "pizza", Value: "12"},
  3596  								},
  3597  								Tests: []*resultstore.Test{
  3598  									{
  3599  										TestType: &resultstore.Test_TestCase{
  3600  											TestCase: &resultstore.TestCase{
  3601  												Timing: &resultstore.Timing{
  3602  													Duration: &durationpb.Duration{
  3603  														Seconds: 60,
  3604  													},
  3605  												},
  3606  												Properties: []*resultstore.Property{
  3607  													{Key: "pizza", Value: "6"},
  3608  												},
  3609  											},
  3610  										},
  3611  									},
  3612  									{
  3613  										TestType: &resultstore.Test_TestCase{
  3614  											TestCase: &resultstore.TestCase{
  3615  												Timing: &resultstore.Timing{
  3616  													Duration: &durationpb.Duration{
  3617  														Seconds: 60,
  3618  													},
  3619  												},
  3620  												Properties: []*resultstore.Property{
  3621  													{Key: "pizza", Value: "6"},
  3622  												},
  3623  											},
  3624  										},
  3625  									},
  3626  								},
  3627  							},
  3628  						},
  3629  					},
  3630  				},
  3631  			},
  3632  			want: map[string]float64{
  3633  				"pizza":                       8,
  3634  				updater.TestMethodsElapsedKey: 2,
  3635  			},
  3636  		},
  3637  		{
  3638  			name: "numeric properties and durations",
  3639  			sar: &singleActionResult{
  3640  				TargetProto: &resultstore.Target{
  3641  					Timing: &resultstore.Timing{
  3642  						Duration: &durationpb.Duration{
  3643  							Seconds: 600,
  3644  						},
  3645  					},
  3646  				},
  3647  			},
  3648  			want: map[string]float64{
  3649  				updater.ElapsedKey: 10,
  3650  			},
  3651  		},
  3652  	}
  3653  
  3654  	for _, tc := range cases {
  3655  		t.Run(tc.name, func(t *testing.T) {
  3656  			got := calculateMetrics(tc.sar)
  3657  			if diff := cmp.Diff(tc.want, got); diff != "" {
  3658  				t.Fatalf("calculateMetrics(...) differed (-want,+got): %s", diff)
  3659  			}
  3660  		})
  3661  	}
  3662  }
  3663  
  3664  func TestShouldUpdate(t *testing.T) {
  3665  	cases := []struct {
  3666  		name         string
  3667  		tg           *configpb.TestGroup
  3668  		clientExists bool
  3669  		want         bool
  3670  	}{
  3671  		{
  3672  			name: "nil",
  3673  			want: false,
  3674  		},
  3675  		{
  3676  			name: "test group config only",
  3677  			tg: &configpb.TestGroup{
  3678  				ResultSource: &configpb.TestGroup_ResultSource{
  3679  					ResultSourceConfig: &configpb.TestGroup_ResultSource_ResultstoreConfig{},
  3680  				},
  3681  			},
  3682  			clientExists: false,
  3683  			want:         false,
  3684  		},
  3685  		{
  3686  			name:         "client only",
  3687  			clientExists: true,
  3688  			want:         false,
  3689  		},
  3690  		{
  3691  			name: "client and non-ResultStore config",
  3692  			tg: &configpb.TestGroup{
  3693  				ResultSource: &configpb.TestGroup_ResultSource{
  3694  					ResultSourceConfig: &configpb.TestGroup_ResultSource_GcsConfig{},
  3695  				},
  3696  			},
  3697  			clientExists: false,
  3698  			want:         false,
  3699  		},
  3700  		{
  3701  			name: "basically works",
  3702  			tg: &configpb.TestGroup{
  3703  				ResultSource: &configpb.TestGroup_ResultSource{
  3704  					ResultSourceConfig: &configpb.TestGroup_ResultSource_ResultstoreConfig{
  3705  						ResultstoreConfig: &configpb.ResultStoreConfig{
  3706  							Project: "my-gcp-project",
  3707  						},
  3708  					},
  3709  				},
  3710  			},
  3711  			clientExists: true,
  3712  			want:         true,
  3713  		},
  3714  	}
  3715  	for _, tc := range cases {
  3716  		t.Run(tc.name, func(t *testing.T) {
  3717  			// Create a fake client if specified.
  3718  			var dlClient *DownloadClient
  3719  			if tc.clientExists {
  3720  				dlClient = &DownloadClient{client: &fakeClient{}}
  3721  			}
  3722  			if got := shouldUpdate(logrus.WithField("name", tc.name), tc.tg, dlClient); tc.want != got {
  3723  				t.Errorf("shouldUpdate(%v, clientExists = %t) got %t, want %t", tc.tg, tc.clientExists, got, tc.want)
  3724  			}
  3725  		})
  3726  	}
  3727  }