go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/services/resultingester/ingest_test_results_test.go (about)

     1  // Copyright 2022 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package resultingester
    16  
    17  import (
    18  	"context"
    19  	"encoding/hex"
    20  	"sort"
    21  	"strings"
    22  	"testing"
    23  	"time"
    24  
    25  	"cloud.google.com/go/spanner"
    26  	"github.com/golang/mock/gomock"
    27  	"google.golang.org/protobuf/proto"
    28  	"google.golang.org/protobuf/types/known/timestamppb"
    29  
    30  	bbpb "go.chromium.org/luci/buildbucket/proto"
    31  	"go.chromium.org/luci/common/clock"
    32  	"go.chromium.org/luci/gae/impl/memory"
    33  	rdbpbutil "go.chromium.org/luci/resultdb/pbutil"
    34  	rdbpb "go.chromium.org/luci/resultdb/proto/v1"
    35  	"go.chromium.org/luci/server/caching"
    36  	"go.chromium.org/luci/server/span"
    37  	"go.chromium.org/luci/server/tq"
    38  	"go.chromium.org/luci/server/tq/tqtesting"
    39  
    40  	"go.chromium.org/luci/analysis/internal/analysis"
    41  	"go.chromium.org/luci/analysis/internal/analysis/clusteredfailures"
    42  	"go.chromium.org/luci/analysis/internal/buildbucket"
    43  	"go.chromium.org/luci/analysis/internal/changepoints"
    44  	"go.chromium.org/luci/analysis/internal/changepoints/bqexporter"
    45  	"go.chromium.org/luci/analysis/internal/changepoints/inputbuffer"
    46  	changepointspb "go.chromium.org/luci/analysis/internal/changepoints/proto"
    47  	"go.chromium.org/luci/analysis/internal/changepoints/testvariantbranch"
    48  	"go.chromium.org/luci/analysis/internal/clustering/chunkstore"
    49  	"go.chromium.org/luci/analysis/internal/clustering/ingestion"
    50  	"go.chromium.org/luci/analysis/internal/config"
    51  	"go.chromium.org/luci/analysis/internal/gerrit"
    52  	"go.chromium.org/luci/analysis/internal/ingestion/control"
    53  	ctrlpb "go.chromium.org/luci/analysis/internal/ingestion/control/proto"
    54  	"go.chromium.org/luci/analysis/internal/resultdb"
    55  	"go.chromium.org/luci/analysis/internal/tasks/taskspb"
    56  	"go.chromium.org/luci/analysis/internal/testresults"
    57  	"go.chromium.org/luci/analysis/internal/testutil"
    58  	"go.chromium.org/luci/analysis/internal/testverdicts"
    59  	"go.chromium.org/luci/analysis/pbutil"
    60  	bqpb "go.chromium.org/luci/analysis/proto/bq"
    61  	configpb "go.chromium.org/luci/analysis/proto/config"
    62  	pb "go.chromium.org/luci/analysis/proto/v1"
    63  
    64  	_ "go.chromium.org/luci/server/tq/txn/spanner"
    65  
    66  	. "github.com/smartystreets/goconvey/convey"
    67  	. "go.chromium.org/luci/common/testing/assertions"
    68  )
    69  
    70  func TestSchedule(t *testing.T) {
    71  	Convey(`TestSchedule`, t, func() {
    72  		ctx := testutil.IntegrationTestContext(t)
    73  		ctx, skdr := tq.TestingContext(ctx, nil)
    74  
    75  		task := &taskspb.IngestTestResults{
    76  			Build:         &ctrlpb.BuildResult{},
    77  			PartitionTime: timestamppb.New(time.Date(2025, time.January, 1, 12, 0, 0, 0, time.UTC)),
    78  		}
    79  		expected := proto.Clone(task).(*taskspb.IngestTestResults)
    80  
    81  		_, err := span.ReadWriteTransaction(ctx, func(ctx context.Context) error {
    82  			Schedule(ctx, task)
    83  			return nil
    84  		})
    85  		So(err, ShouldBeNil)
    86  		So(skdr.Tasks().Payloads()[0], ShouldResembleProto, expected)
    87  	})
    88  }
    89  
    90  const testInvocation = "invocations/build-87654321"
    91  const testRealm = "project:ci"
    92  const testBuildID = int64(87654321)
    93  
    94  func TestIngestTestResults(t *testing.T) {
    95  	Convey(`TestIngestTestResults`, t, func() {
    96  		ctx := testutil.IntegrationTestContext(t)
    97  		ctx = caching.WithEmptyProcessCache(ctx) // For failure association rules cache.
    98  		ctx, skdr := tq.TestingContext(ctx, nil)
    99  		ctx = memory.Use(ctx)
   100  
   101  		chunkStore := chunkstore.NewFakeClient()
   102  		clusteredFailures := clusteredfailures.NewFakeClient()
   103  		testVerdicts := testverdicts.NewFakeClient()
   104  		tvBQExporterClient := bqexporter.NewFakeClient()
   105  		analysis := analysis.NewClusteringHandler(clusteredFailures)
   106  		ri := &resultIngester{
   107  			clustering:                ingestion.New(chunkStore, analysis),
   108  			verdictExporter:           testverdicts.NewExporter(testVerdicts),
   109  			testVariantBranchExporter: bqexporter.NewExporter(tvBQExporterClient),
   110  		}
   111  
   112  		Convey(`partition time`, func() {
   113  			payload := &taskspb.IngestTestResults{
   114  				Build: &ctrlpb.BuildResult{
   115  					Host:    "host",
   116  					Id:      13131313,
   117  					Project: "project",
   118  				},
   119  				PartitionTime: timestamppb.New(clock.Now(ctx).Add(-1 * time.Hour)),
   120  			}
   121  			Convey(`too early`, func() {
   122  				payload.PartitionTime = timestamppb.New(clock.Now(ctx).Add(25 * time.Hour))
   123  				err := ri.ingestTestResults(ctx, payload)
   124  				So(err, ShouldErrLike, "too far in the future")
   125  			})
   126  			Convey(`too late`, func() {
   127  				payload.PartitionTime = timestamppb.New(clock.Now(ctx).Add(-91 * 24 * time.Hour))
   128  				err := ri.ingestTestResults(ctx, payload)
   129  				So(err, ShouldErrLike, "too long ago")
   130  			})
   131  		})
   132  
   133  		Convey(`valid payload`, func() {
   134  			ctl := gomock.NewController(t)
   135  			defer ctl.Finish()
   136  
   137  			mrc := resultdb.NewMockedClient(ctx, ctl)
   138  			mbc := buildbucket.NewMockedClient(mrc.Ctx, ctl)
   139  			ctx = mbc.Ctx
   140  
   141  			clsByHost := gerritChangesByHostForTesting()
   142  			ctx = gerrit.UseFakeClient(ctx, clsByHost)
   143  
   144  			bHost := "host"
   145  			partitionTime := clock.Now(ctx).Add(-1 * time.Hour)
   146  
   147  			setupGetInvocationMock := func() {
   148  				invReq := &rdbpb.GetInvocationRequest{
   149  					Name: testInvocation,
   150  				}
   151  				invRes := &rdbpb.Invocation{
   152  					Name:  testInvocation,
   153  					Realm: testRealm,
   154  				}
   155  				mrc.GetInvocation(invReq, invRes)
   156  			}
   157  
   158  			setupQueryTestVariantsMock := func(modifiers ...func(*rdbpb.QueryTestVariantsResponse)) {
   159  				tvReq := &rdbpb.QueryTestVariantsRequest{
   160  					Invocations: []string{testInvocation},
   161  					PageSize:    10000,
   162  					ResultLimit: 100,
   163  					ReadMask:    testVariantReadMask,
   164  					PageToken:   "expected_token",
   165  				}
   166  				tvRsp := mockedQueryTestVariantsRsp()
   167  				tvRsp.NextPageToken = "continuation_token"
   168  				for _, modifier := range modifiers {
   169  					modifier(tvRsp)
   170  				}
   171  				mrc.QueryTestVariants(tvReq, tvRsp)
   172  			}
   173  
   174  			cfg := &configpb.Config{
   175  				TestVariantAnalysis: &configpb.TestVariantAnalysis{
   176  					Enabled:               true,
   177  					BigqueryExportEnabled: true,
   178  				},
   179  				TestVerdictExport: &configpb.TestVerdictExport{
   180  					Enabled: true,
   181  				},
   182  				Clustering: &configpb.ClusteringSystem{
   183  					QueryTestVariantAnalysisEnabled: true,
   184  				},
   185  			}
   186  			setupConfig := func(ctx context.Context, cfg *configpb.Config) {
   187  				err := config.SetTestConfig(ctx, cfg)
   188  				So(err, ShouldBeNil)
   189  			}
   190  
   191  			// Populate some existing test variant analysis.
   192  			setupTestVariantAnalysis(ctx, partitionTime)
   193  
   194  			payload := &taskspb.IngestTestResults{
   195  				Build: &ctrlpb.BuildResult{
   196  					Host:         bHost,
   197  					Id:           testBuildID,
   198  					CreationTime: timestamppb.New(time.Date(2020, time.April, 1, 2, 3, 4, 5, time.UTC)),
   199  					Project:      "project",
   200  					Bucket:       "bucket",
   201  					Builder:      "builder",
   202  					Status:       pb.BuildStatus_BUILD_STATUS_FAILURE,
   203  					Changelists: []*pb.Changelist{
   204  						{
   205  							Host:      "anothergerrit.gerrit.instance",
   206  							Change:    77788,
   207  							Patchset:  19,
   208  							OwnerKind: pb.ChangelistOwnerKind_HUMAN,
   209  						},
   210  						{
   211  							Host:      "mygerrit-review.googlesource.com",
   212  							Change:    12345,
   213  							Patchset:  5,
   214  							OwnerKind: pb.ChangelistOwnerKind_AUTOMATION,
   215  						},
   216  					},
   217  					Commit: &bbpb.GitilesCommit{
   218  						Host:     "myproject.googlesource.com",
   219  						Project:  "someproject/src",
   220  						Id:       strings.Repeat("0a", 20),
   221  						Ref:      "refs/heads/mybranch",
   222  						Position: 111888,
   223  					},
   224  					HasInvocation:        true,
   225  					ResultdbHost:         "results.api.cr.dev",
   226  					IsIncludedByAncestor: false,
   227  					GardenerRotations:    []string{"rotation1", "rotation2"},
   228  				},
   229  				PartitionTime: timestamppb.New(partitionTime),
   230  				PresubmitRun: &ctrlpb.PresubmitResult{
   231  					PresubmitRunId: &pb.PresubmitRunId{
   232  						System: "luci-cv",
   233  						Id:     "infra/12345",
   234  					},
   235  					Status:       pb.PresubmitRunStatus_PRESUBMIT_RUN_STATUS_SUCCEEDED,
   236  					Mode:         pb.PresubmitRunMode_FULL_RUN,
   237  					Owner:        "automation",
   238  					CreationTime: timestamppb.New(time.Date(2021, time.April, 1, 2, 3, 4, 5, time.UTC)),
   239  				},
   240  				PageToken: "expected_token",
   241  				TaskIndex: 0,
   242  			}
   243  			expectedContinuation := proto.Clone(payload).(*taskspb.IngestTestResults)
   244  			expectedContinuation.PageToken = "continuation_token"
   245  			expectedContinuation.TaskIndex = 1
   246  
   247  			ingestionCtl :=
   248  				control.NewEntry(0).
   249  					WithBuildID(control.BuildID(bHost, testBuildID)).
   250  					WithBuildResult(proto.Clone(payload.Build).(*ctrlpb.BuildResult)).
   251  					WithPresubmitResult(proto.Clone(payload.PresubmitRun).(*ctrlpb.PresubmitResult)).
   252  					WithTaskCount(1).
   253  					Build()
   254  
   255  			Convey(`First task`, func() {
   256  				setupGetInvocationMock()
   257  				setupQueryTestVariantsMock()
   258  				setupConfig(ctx, cfg)
   259  				_, err := control.SetEntriesForTesting(ctx, ingestionCtl)
   260  				So(err, ShouldBeNil)
   261  
   262  				// Act
   263  				err = ri.ingestTestResults(ctx, payload)
   264  				So(err, ShouldBeNil)
   265  
   266  				// Verify
   267  
   268  				// Expect a continuation task to be created.
   269  				verifyContinuationTask(skdr, expectedContinuation)
   270  				ingestionCtl.TaskCount = ingestionCtl.TaskCount + 1 // Expect to have been incremented.
   271  				verifyIngestionControl(ctx, ingestionCtl)
   272  				verifyTestResults(ctx, partitionTime)
   273  				verifyClustering(chunkStore, clusteredFailures)
   274  				verifyTestVerdicts(testVerdicts, partitionTime)
   275  				verifyTestVariantAnalysis(ctx, partitionTime, tvBQExporterClient)
   276  			})
   277  			Convey(`Last task`, func() {
   278  				payload.TaskIndex = 10
   279  				ingestionCtl.TaskCount = 11
   280  
   281  				setupGetInvocationMock()
   282  				setupQueryTestVariantsMock(func(rsp *rdbpb.QueryTestVariantsResponse) {
   283  					rsp.NextPageToken = ""
   284  				})
   285  				setupConfig(ctx, cfg)
   286  
   287  				_, err := control.SetEntriesForTesting(ctx, ingestionCtl)
   288  				So(err, ShouldBeNil)
   289  
   290  				// Act
   291  				err = ri.ingestTestResults(ctx, payload)
   292  				So(err, ShouldBeNil)
   293  
   294  				// Verify
   295  
   296  				// As this is the last task, do not expect a continuation
   297  				// task to be created.
   298  				verifyContinuationTask(skdr, nil)
   299  				verifyIngestionControl(ctx, ingestionCtl)
   300  				verifyTestResults(ctx, partitionTime)
   301  				verifyClustering(chunkStore, clusteredFailures)
   302  				verifyTestVerdicts(testVerdicts, partitionTime)
   303  				verifyTestVariantAnalysis(ctx, partitionTime, tvBQExporterClient)
   304  			})
   305  
   306  			Convey(`Retry task after continuation task already created`, func() {
   307  				// Scenario: First task fails after it has already scheduled
   308  				// its continuation.
   309  				ingestionCtl.TaskCount = 2
   310  
   311  				setupGetInvocationMock()
   312  				setupQueryTestVariantsMock()
   313  				setupConfig(ctx, cfg)
   314  
   315  				_, err := control.SetEntriesForTesting(ctx, ingestionCtl)
   316  				So(err, ShouldBeNil)
   317  
   318  				// Act
   319  				err = ri.ingestTestResults(ctx, payload)
   320  				So(err, ShouldBeNil)
   321  
   322  				// Verify
   323  
   324  				// Do not expect a continuation task to be created,
   325  				// as it was already scheduled.
   326  				verifyContinuationTask(skdr, nil)
   327  				verifyIngestionControl(ctx, ingestionCtl)
   328  				verifyTestResults(ctx, partitionTime)
   329  				verifyClustering(chunkStore, clusteredFailures)
   330  				verifyTestVerdicts(testVerdicts, partitionTime)
   331  				verifyTestVariantAnalysis(ctx, partitionTime, tvBQExporterClient)
   332  			})
   333  			Convey(`No project config`, func() {
   334  				// If no project config exists, results should be ingested into
   335  				// TestResults and clustered, but not used for the legacy test variant
   336  				// analysis.
   337  				config.SetTestProjectConfig(ctx, map[string]*configpb.ProjectConfig{})
   338  
   339  				setupGetInvocationMock()
   340  				setupQueryTestVariantsMock()
   341  				setupConfig(ctx, cfg)
   342  
   343  				_, err := control.SetEntriesForTesting(ctx, ingestionCtl)
   344  				So(err, ShouldBeNil)
   345  
   346  				// Act
   347  				err = ri.ingestTestResults(ctx, payload)
   348  				So(err, ShouldBeNil)
   349  
   350  				// Verify
   351  				// Test results still ingested.
   352  				verifyTestResults(ctx, partitionTime)
   353  
   354  				// Cluster has happened.
   355  				verifyClustering(chunkStore, clusteredFailures)
   356  
   357  				// Test verdicts exported.
   358  				verifyTestVerdicts(testVerdicts, partitionTime)
   359  				verifyTestVariantAnalysis(ctx, partitionTime, tvBQExporterClient)
   360  			})
   361  			Convey(`Build included by ancestor`, func() {
   362  				payload.Build.IsIncludedByAncestor = true
   363  				setupConfig(ctx, cfg)
   364  
   365  				// Act
   366  				err := ri.ingestTestResults(ctx, payload)
   367  				So(err, ShouldBeNil)
   368  
   369  				// Verify no test results ingested into test history.
   370  				var actualTRs []*testresults.TestResult
   371  				err = testresults.ReadTestResults(span.Single(ctx), spanner.AllKeys(), func(tr *testresults.TestResult) error {
   372  					actualTRs = append(actualTRs, tr)
   373  					return nil
   374  				})
   375  				So(err, ShouldBeNil)
   376  				So(actualTRs, ShouldHaveLength, 0)
   377  			})
   378  			Convey(`Project not allowed`, func() {
   379  				cfg.Ingestion = &configpb.Ingestion{
   380  					ProjectAllowlistEnabled: true,
   381  					ProjectAllowlist:        []string{"other"},
   382  				}
   383  				setupConfig(ctx, cfg)
   384  
   385  				// Act
   386  				err := ri.ingestTestResults(ctx, payload)
   387  				So(err, ShouldBeNil)
   388  
   389  				// Verify no test results ingested into test history.
   390  				var actualTRs []*testresults.TestResult
   391  				err = testresults.ReadTestResults(span.Single(ctx), spanner.AllKeys(), func(tr *testresults.TestResult) error {
   392  					actualTRs = append(actualTRs, tr)
   393  					return nil
   394  				})
   395  				So(err, ShouldBeNil)
   396  				So(actualTRs, ShouldHaveLength, 0)
   397  			})
   398  		})
   399  	})
   400  }
   401  
   402  func setupTestVariantAnalysis(ctx context.Context, partitionTime time.Time) {
   403  	sr := &pb.SourceRef{
   404  		System: &pb.SourceRef_Gitiles{
   405  			Gitiles: &pb.GitilesRef{
   406  				Host:    "project.googlesource.com",
   407  				Project: "myproject/src",
   408  				Ref:     "refs/heads/main",
   409  			},
   410  		},
   411  	}
   412  	// Truncated to nearest hour.
   413  	hour := partitionTime.Unix() / 3600
   414  
   415  	branch := &testvariantbranch.Entry{
   416  		IsNew:       true,
   417  		Project:     "project",
   418  		TestID:      "ninja://test_consistent_failure",
   419  		VariantHash: "hash",
   420  		SourceRef:   sr,
   421  		RefHash:     rdbpbutil.SourceRefHash(pbutil.SourceRefToResultDB(sr)),
   422  		InputBuffer: &inputbuffer.Buffer{
   423  			HotBufferCapacity:  100,
   424  			ColdBufferCapacity: 2000,
   425  			HotBuffer: inputbuffer.History{
   426  				Verdicts: []inputbuffer.PositionVerdict{},
   427  			},
   428  			ColdBuffer: inputbuffer.History{
   429  				Verdicts: []inputbuffer.PositionVerdict{},
   430  			},
   431  		},
   432  		Statistics: &changepointspb.Statistics{
   433  			HourlyBuckets: []*changepointspb.Statistics_HourBucket{
   434  				{
   435  					Hour:               int64(hour - 23),
   436  					UnexpectedVerdicts: 123,
   437  					FlakyVerdicts:      456,
   438  					TotalVerdicts:      1999,
   439  				},
   440  			},
   441  		},
   442  	}
   443  	var hs inputbuffer.HistorySerializer
   444  	m, err := branch.ToMutation(&hs)
   445  	So(err, ShouldBeNil)
   446  	testutil.MustApply(ctx, m)
   447  }
   448  
   449  func verifyTestVariantAnalysis(ctx context.Context, partitionTime time.Time, client *bqexporter.FakeClient) {
   450  	tvbs, err := changepoints.FetchTestVariantBranches(ctx)
   451  	So(err, ShouldBeNil)
   452  	So(len(tvbs), ShouldEqual, 1)
   453  	sr := &pb.SourceRef{
   454  		System: &pb.SourceRef_Gitiles{
   455  			Gitiles: &pb.GitilesRef{
   456  				Host:    "project.googlesource.com",
   457  				Project: "myproject/src",
   458  				Ref:     "refs/heads/main",
   459  			},
   460  		},
   461  	}
   462  	// Truncated to nearest hour.
   463  	hour := time.Unix(partitionTime.Unix()/3600*3600, 0)
   464  
   465  	So(tvbs[0], ShouldResembleProto, &testvariantbranch.Entry{
   466  		Project:     "project",
   467  		TestID:      "ninja://test_consistent_failure",
   468  		VariantHash: "hash",
   469  		SourceRef:   sr,
   470  		RefHash:     rdbpbutil.SourceRefHash(pbutil.SourceRefToResultDB(sr)),
   471  		InputBuffer: &inputbuffer.Buffer{
   472  			HotBufferCapacity:  100,
   473  			ColdBufferCapacity: 2000,
   474  			HotBuffer: inputbuffer.History{
   475  				Verdicts: []inputbuffer.PositionVerdict{
   476  					{
   477  						CommitPosition: 16801,
   478  						Hour:           hour,
   479  						Details: inputbuffer.VerdictDetails{
   480  							IsExonerated: true,
   481  							Runs: []inputbuffer.Run{
   482  								{
   483  									Unexpected: inputbuffer.ResultCounts{
   484  										FailCount: 1,
   485  									},
   486  								},
   487  							},
   488  						},
   489  					},
   490  				},
   491  			},
   492  			ColdBuffer: inputbuffer.History{
   493  				Verdicts: []inputbuffer.PositionVerdict{},
   494  			},
   495  		},
   496  		Statistics: &changepointspb.Statistics{
   497  			HourlyBuckets: []*changepointspb.Statistics_HourBucket{
   498  				{
   499  					Hour:               int64(hour.Unix()/3600 - 23),
   500  					UnexpectedVerdicts: 123,
   501  					FlakyVerdicts:      456,
   502  					TotalVerdicts:      1999,
   503  				},
   504  			},
   505  		},
   506  	})
   507  
   508  	So(len(client.Insertions), ShouldEqual, 1)
   509  }
   510  
   511  func verifyTestResults(ctx context.Context, expectedPartitionTime time.Time) {
   512  	trBuilder := testresults.NewTestResult().
   513  		WithProject("project").
   514  		WithPartitionTime(expectedPartitionTime.In(time.UTC)).
   515  		WithIngestedInvocationID("build-87654321").
   516  		WithSubRealm("ci").
   517  		WithSources(testresults.Sources{
   518  			Changelists: []testresults.Changelist{
   519  				{
   520  					Host:      "anothergerrit.gerrit.instance",
   521  					Change:    77788,
   522  					Patchset:  19,
   523  					OwnerKind: pb.ChangelistOwnerKind_HUMAN,
   524  				},
   525  				{
   526  					Host:      "mygerrit-review.googlesource.com",
   527  					Change:    12345,
   528  					Patchset:  5,
   529  					OwnerKind: pb.ChangelistOwnerKind_AUTOMATION,
   530  				},
   531  			},
   532  		})
   533  
   534  	rdbSources := testresults.Sources{
   535  		RefHash: pbutil.SourceRefHash(&pb.SourceRef{
   536  			System: &pb.SourceRef_Gitiles{
   537  				Gitiles: &pb.GitilesRef{
   538  					Host:    "project.googlesource.com",
   539  					Project: "myproject/src",
   540  					Ref:     "refs/heads/main",
   541  				},
   542  			},
   543  		}),
   544  		Position: 16801,
   545  		Changelists: []testresults.Changelist{
   546  			{
   547  				Host:      "project-review.googlesource.com",
   548  				Change:    9991,
   549  				Patchset:  82,
   550  				OwnerKind: pb.ChangelistOwnerKind_HUMAN,
   551  			},
   552  		},
   553  	}
   554  
   555  	expectedTRs := []*testresults.TestResult{
   556  		trBuilder.WithTestID("ninja://test_consistent_failure").
   557  			WithVariantHash("hash").
   558  			WithRunIndex(0).
   559  			WithResultIndex(0).
   560  			WithIsUnexpected(true).
   561  			WithStatus(pb.TestResultStatus_FAIL).
   562  			WithRunDuration(3*time.Second+1*time.Microsecond).
   563  			WithExonerationReasons(pb.ExonerationReason_OCCURS_ON_OTHER_CLS, pb.ExonerationReason_NOT_CRITICAL, pb.ExonerationReason_OCCURS_ON_MAINLINE).
   564  			WithIsFromBisection(false).
   565  			WithSources(rdbSources).
   566  			Build(),
   567  		trBuilder.WithTestID("ninja://test_expected").
   568  			WithVariantHash("hash").
   569  			WithRunIndex(0).
   570  			WithResultIndex(0).
   571  			WithIsUnexpected(false).
   572  			WithStatus(pb.TestResultStatus_PASS).
   573  			WithRunDuration(5 * time.Second).
   574  			WithoutExoneration().
   575  			WithIsFromBisection(false).
   576  			Build(),
   577  		trBuilder.WithTestID("ninja://test_filtering_event").
   578  			WithVariantHash("hash").
   579  			WithRunIndex(0).
   580  			WithResultIndex(0).
   581  			WithIsUnexpected(false).
   582  			WithStatus(pb.TestResultStatus_SKIP).
   583  			WithoutRunDuration().
   584  			WithoutExoneration().
   585  			WithIsFromBisection(false).
   586  			Build(),
   587  		trBuilder.WithTestID("ninja://test_from_luci_bisection").
   588  			WithVariantHash("hash").
   589  			WithRunIndex(0).
   590  			WithResultIndex(0).
   591  			WithIsUnexpected(true).
   592  			WithStatus(pb.TestResultStatus_PASS).
   593  			WithoutRunDuration().
   594  			WithoutExoneration().
   595  			WithIsFromBisection(true).
   596  			Build(),
   597  		trBuilder.WithTestID("ninja://test_has_unexpected").
   598  			WithVariantHash("hash").
   599  			WithRunIndex(0).
   600  			WithResultIndex(0).
   601  			WithIsUnexpected(true).
   602  			WithStatus(pb.TestResultStatus_FAIL).
   603  			WithoutRunDuration().
   604  			WithoutExoneration().
   605  			WithIsFromBisection(false).
   606  			Build(),
   607  		trBuilder.WithTestID("ninja://test_has_unexpected").
   608  			WithVariantHash("hash").
   609  			WithRunIndex(1).
   610  			WithResultIndex(0).
   611  			WithIsUnexpected(false).
   612  			WithStatus(pb.TestResultStatus_PASS).
   613  			WithoutRunDuration().
   614  			WithoutExoneration().
   615  			WithIsFromBisection(false).
   616  			Build(),
   617  		trBuilder.WithTestID("ninja://test_known_flake").
   618  			WithVariantHash("hash_2").
   619  			WithRunIndex(0).
   620  			WithResultIndex(0).
   621  			WithIsUnexpected(true).
   622  			WithStatus(pb.TestResultStatus_FAIL).
   623  			WithRunDuration(2 * time.Second).
   624  			WithoutExoneration().
   625  			WithIsFromBisection(false).
   626  			Build(),
   627  		trBuilder.WithTestID("ninja://test_new_failure").
   628  			WithVariantHash("hash_1").
   629  			WithRunIndex(0).
   630  			WithResultIndex(0).
   631  			WithIsUnexpected(true).
   632  			WithStatus(pb.TestResultStatus_FAIL).
   633  			WithRunDuration(1 * time.Second).
   634  			WithoutExoneration().
   635  			WithIsFromBisection(false).
   636  			Build(),
   637  		trBuilder.WithTestID("ninja://test_new_flake").
   638  			WithVariantHash("hash").
   639  			WithRunIndex(0).
   640  			WithResultIndex(0).
   641  			WithIsUnexpected(true).
   642  			WithStatus(pb.TestResultStatus_FAIL).
   643  			WithRunDuration(10 * time.Second).
   644  			WithoutExoneration().
   645  			WithIsFromBisection(false).
   646  			Build(),
   647  		trBuilder.WithTestID("ninja://test_new_flake").
   648  			WithVariantHash("hash").
   649  			WithRunIndex(0).
   650  			WithResultIndex(1).
   651  			WithIsUnexpected(true).
   652  			WithStatus(pb.TestResultStatus_FAIL).
   653  			WithRunDuration(11 * time.Second).
   654  			WithoutExoneration().
   655  			WithIsFromBisection(false).
   656  			Build(),
   657  		trBuilder.WithTestID("ninja://test_new_flake").
   658  			WithVariantHash("hash").
   659  			WithRunIndex(1).
   660  			WithResultIndex(0).
   661  			WithIsUnexpected(false).
   662  			WithStatus(pb.TestResultStatus_PASS).
   663  			WithRunDuration(12 * time.Second).
   664  			WithoutExoneration().
   665  			WithIsFromBisection(false).
   666  			Build(),
   667  		trBuilder.WithTestID("ninja://test_no_new_results").
   668  			WithVariantHash("hash").
   669  			WithRunIndex(0).
   670  			WithResultIndex(0).
   671  			WithIsUnexpected(true).
   672  			WithStatus(pb.TestResultStatus_FAIL).
   673  			WithRunDuration(4 * time.Second).
   674  			WithoutExoneration().
   675  			WithIsFromBisection(false).
   676  			Build(),
   677  		trBuilder.WithTestID("ninja://test_skip").
   678  			WithVariantHash("hash").
   679  			WithRunIndex(0).
   680  			WithResultIndex(0).
   681  			WithIsUnexpected(true).
   682  			WithStatus(pb.TestResultStatus_SKIP).
   683  			WithoutRunDuration().
   684  			WithoutExoneration().
   685  			WithIsFromBisection(false).
   686  			Build(),
   687  		trBuilder.WithTestID("ninja://test_unexpected_pass").
   688  			WithVariantHash("hash").
   689  			WithRunIndex(0).
   690  			WithResultIndex(0).
   691  			WithIsUnexpected(true).
   692  			WithStatus(pb.TestResultStatus_PASS).
   693  			WithoutRunDuration().
   694  			WithoutExoneration().
   695  			WithIsFromBisection(false).
   696  			Build(),
   697  	}
   698  
   699  	// Validate TestResults table is populated.
   700  	var actualTRs []*testresults.TestResult
   701  	err := testresults.ReadTestResults(span.Single(ctx), spanner.AllKeys(), func(tr *testresults.TestResult) error {
   702  		actualTRs = append(actualTRs, tr)
   703  		return nil
   704  	})
   705  	So(err, ShouldBeNil)
   706  	So(actualTRs, ShouldResemble, expectedTRs)
   707  
   708  	// Validate TestVariantRealms table is populated.
   709  	tvrs := make([]*testresults.TestVariantRealm, 0)
   710  	err = testresults.ReadTestVariantRealms(span.Single(ctx), spanner.AllKeys(), func(tvr *testresults.TestVariantRealm) error {
   711  		tvrs = append(tvrs, tvr)
   712  		return nil
   713  	})
   714  	So(err, ShouldBeNil)
   715  
   716  	expectedRealms := []*testresults.TestVariantRealm{
   717  		{
   718  			Project:     "project",
   719  			TestID:      "ninja://test_consistent_failure",
   720  			VariantHash: "hash",
   721  			SubRealm:    "ci",
   722  			Variant:     nil,
   723  		},
   724  		{
   725  			Project:     "project",
   726  			TestID:      "ninja://test_expected",
   727  			VariantHash: "hash",
   728  			SubRealm:    "ci",
   729  			Variant:     nil,
   730  		},
   731  		{
   732  			Project:     "project",
   733  			TestID:      "ninja://test_filtering_event",
   734  			VariantHash: "hash",
   735  			SubRealm:    "ci",
   736  			Variant:     nil,
   737  		},
   738  		{
   739  			Project:     "project",
   740  			TestID:      "ninja://test_from_luci_bisection",
   741  			VariantHash: "hash",
   742  			SubRealm:    "ci",
   743  			Variant:     nil,
   744  		},
   745  		{
   746  			Project:     "project",
   747  			TestID:      "ninja://test_has_unexpected",
   748  			VariantHash: "hash",
   749  			SubRealm:    "ci",
   750  			Variant:     nil,
   751  		},
   752  		{
   753  			Project:     "project",
   754  			TestID:      "ninja://test_known_flake",
   755  			VariantHash: "hash_2",
   756  			SubRealm:    "ci",
   757  			Variant:     pbutil.VariantFromResultDB(rdbpbutil.Variant("k1", "v2")),
   758  		},
   759  		{
   760  			Project:     "project",
   761  			TestID:      "ninja://test_new_failure",
   762  			VariantHash: "hash_1",
   763  			SubRealm:    "ci",
   764  			Variant:     pbutil.VariantFromResultDB(rdbpbutil.Variant("k1", "v1")),
   765  		},
   766  		{
   767  			Project:     "project",
   768  			TestID:      "ninja://test_new_flake",
   769  			VariantHash: "hash",
   770  			SubRealm:    "ci",
   771  			Variant:     nil,
   772  		},
   773  		{
   774  			Project:     "project",
   775  			TestID:      "ninja://test_no_new_results",
   776  			VariantHash: "hash",
   777  			SubRealm:    "ci",
   778  			Variant:     nil,
   779  		},
   780  		{
   781  			Project:     "project",
   782  			TestID:      "ninja://test_skip",
   783  			VariantHash: "hash",
   784  			SubRealm:    "ci",
   785  			Variant:     nil,
   786  		},
   787  		{
   788  			Project:     "project",
   789  			TestID:      "ninja://test_unexpected_pass",
   790  			VariantHash: "hash",
   791  			SubRealm:    "ci",
   792  			Variant:     nil,
   793  		},
   794  	}
   795  
   796  	So(tvrs, ShouldHaveLength, len(expectedRealms))
   797  	for i, tvr := range tvrs {
   798  		expectedTVR := expectedRealms[i]
   799  		So(tvr.LastIngestionTime, ShouldNotBeZeroValue)
   800  		expectedTVR.LastIngestionTime = tvr.LastIngestionTime
   801  		So(tvr, ShouldResemble, expectedTVR)
   802  	}
   803  
   804  	// Validate TestRealms table is populated.
   805  	testRealms := make([]*testresults.TestRealm, 0)
   806  	err = testresults.ReadTestRealms(span.Single(ctx), spanner.AllKeys(), func(tvr *testresults.TestRealm) error {
   807  		testRealms = append(testRealms, tvr)
   808  		return nil
   809  	})
   810  	So(err, ShouldBeNil)
   811  
   812  	// The order of test realms doesn't matter. Sort it to make comparing it
   813  	// against the expected test realm list easier.
   814  	sort.Slice(testRealms, func(i, j int) bool {
   815  		item1 := testRealms[i]
   816  		item2 := testRealms[j]
   817  		if item1.Project != item2.Project {
   818  			return item1.Project <= item2.Project
   819  		}
   820  		if item1.SubRealm != item2.SubRealm {
   821  			return item1.SubRealm <= item2.SubRealm
   822  		}
   823  		if item1.TestID != item2.TestID {
   824  			return item1.TestID <= item2.TestID
   825  		}
   826  		return false
   827  	})
   828  
   829  	expectedTestRealms := []*testresults.TestRealm{
   830  		{
   831  			Project:  "project",
   832  			TestID:   "ninja://test_consistent_failure",
   833  			SubRealm: "ci",
   834  		},
   835  		{
   836  			Project:  "project",
   837  			TestID:   "ninja://test_expected",
   838  			SubRealm: "ci",
   839  		},
   840  		{
   841  			Project:  "project",
   842  			TestID:   "ninja://test_filtering_event",
   843  			SubRealm: "ci",
   844  		},
   845  		{
   846  			Project:  "project",
   847  			TestID:   "ninja://test_from_luci_bisection",
   848  			SubRealm: "ci",
   849  		},
   850  		{
   851  			Project:  "project",
   852  			TestID:   "ninja://test_has_unexpected",
   853  			SubRealm: "ci",
   854  		},
   855  		{
   856  			Project:  "project",
   857  			TestID:   "ninja://test_known_flake",
   858  			SubRealm: "ci",
   859  		},
   860  		{
   861  			Project:  "project",
   862  			TestID:   "ninja://test_new_failure",
   863  			SubRealm: "ci",
   864  		},
   865  		{
   866  			Project:  "project",
   867  			TestID:   "ninja://test_new_flake",
   868  			SubRealm: "ci",
   869  		},
   870  		{
   871  			Project:  "project",
   872  			TestID:   "ninja://test_no_new_results",
   873  			SubRealm: "ci",
   874  		},
   875  		{
   876  			Project:  "project",
   877  			TestID:   "ninja://test_skip",
   878  			SubRealm: "ci",
   879  		},
   880  		{
   881  			Project:  "project",
   882  			TestID:   "ninja://test_unexpected_pass",
   883  			SubRealm: "ci",
   884  		},
   885  	}
   886  
   887  	So(testRealms, ShouldHaveLength, len(expectedTestRealms))
   888  	for i, tr := range testRealms {
   889  		expectedTR := expectedTestRealms[i]
   890  		So(tr.LastIngestionTime, ShouldNotBeZeroValue)
   891  		expectedTR.LastIngestionTime = tr.LastIngestionTime
   892  		So(tr, ShouldResemble, expectedTR)
   893  	}
   894  }
   895  
   896  func verifyClustering(chunkStore *chunkstore.FakeClient, clusteredFailures *clusteredfailures.FakeClient) {
   897  	// Confirm chunks have been written to GCS.
   898  	So(len(chunkStore.Contents), ShouldEqual, 1)
   899  
   900  	// Confirm clustering has occurred, with each test result in at
   901  	// least one cluster.
   902  	actualClusteredFailures := make(map[string]int)
   903  	for _, f := range clusteredFailures.Insertions {
   904  		So(f.Project, ShouldEqual, "project")
   905  		actualClusteredFailures[f.TestId] += 1
   906  	}
   907  	expectedClusteredFailures := map[string]int{
   908  		"ninja://test_new_failure":        1,
   909  		"ninja://test_known_flake":        1,
   910  		"ninja://test_consistent_failure": 2, // One failure is in two clusters due it having a failure reason.
   911  		"ninja://test_no_new_results":     1,
   912  		"ninja://test_new_flake":          2,
   913  		"ninja://test_has_unexpected":     1,
   914  	}
   915  	So(actualClusteredFailures, ShouldResemble, expectedClusteredFailures)
   916  
   917  	for _, cf := range clusteredFailures.Insertions {
   918  		So(cf.BuildGardenerRotations, ShouldResemble, []string{"rotation1", "rotation2"})
   919  
   920  		// Verify test variant branch stats were correctly populated.
   921  		if cf.TestId == "ninja://test_consistent_failure" {
   922  			So(cf.TestVariantBranch, ShouldResembleProto, &bqpb.ClusteredFailureRow_TestVariantBranch{
   923  				UnexpectedVerdicts_24H: 124,
   924  				FlakyVerdicts_24H:      456,
   925  				TotalVerdicts_24H:      2000,
   926  			})
   927  		} else {
   928  			So(cf.TestVariantBranch, ShouldBeNil)
   929  		}
   930  	}
   931  }
   932  
   933  func verifyTestVerdicts(client *testverdicts.FakeClient, expectedPartitionTime time.Time) {
   934  	actualRows := client.Insertions
   935  
   936  	invocation := &bqpb.TestVerdictRow_InvocationRecord{
   937  		Id:         "build-87654321",
   938  		Realm:      "project:ci",
   939  		Properties: "{}",
   940  	}
   941  
   942  	testMetadata := &pb.TestMetadata{
   943  		Name: "updated_name",
   944  		Location: &pb.TestLocation{
   945  			Repo:     "repo",
   946  			FileName: "file_name",
   947  			Line:     456,
   948  		},
   949  		BugComponent: &pb.BugComponent{
   950  			System: &pb.BugComponent_IssueTracker{
   951  				IssueTracker: &pb.IssueTrackerComponent{
   952  					ComponentId: 12345,
   953  				},
   954  			},
   955  		},
   956  	}
   957  
   958  	buildbucketBuild := &bqpb.TestVerdictRow_BuildbucketBuild{
   959  		Id: testBuildID,
   960  		Builder: &bqpb.TestVerdictRow_BuildbucketBuild_Builder{
   961  			Project: "project",
   962  			Bucket:  "bucket",
   963  			Builder: "builder",
   964  		},
   965  		Status:            "FAILURE",
   966  		GardenerRotations: []string{"rotation1", "rotation2"},
   967  	}
   968  
   969  	cvRun := &bqpb.TestVerdictRow_ChangeVerifierRun{
   970  		Id:              "infra/12345",
   971  		Mode:            pb.PresubmitRunMode_FULL_RUN,
   972  		Status:          "SUCCEEDED",
   973  		IsBuildCritical: false,
   974  	}
   975  
   976  	// Different platforms may use different spacing when serializing
   977  	// JSONPB. Expect the spacing scheme used by this platform.
   978  	expectedProperties, err := testverdicts.MarshalStructPB(testProperties)
   979  	So(err, ShouldBeNil)
   980  
   981  	sr := &pb.SourceRef{
   982  		System: &pb.SourceRef_Gitiles{
   983  			Gitiles: &pb.GitilesRef{
   984  				Host:    "project.googlesource.com",
   985  				Project: "myproject/src",
   986  				Ref:     "refs/heads/main",
   987  			},
   988  		},
   989  	}
   990  	expectedRows := []*bqpb.TestVerdictRow{
   991  		{
   992  			Project:       "project",
   993  			TestId:        "ninja://test_consistent_failure",
   994  			Variant:       "{}",
   995  			VariantHash:   "hash",
   996  			Invocation:    invocation,
   997  			PartitionTime: timestamppb.New(expectedPartitionTime),
   998  			Status:        pb.TestVerdictStatus_EXONERATED,
   999  			Results: []*bqpb.TestVerdictRow_TestResult{
  1000  				{
  1001  					Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{
  1002  						Id: "build-1234",
  1003  					},
  1004  					Name:        "invocations/build-1234/tests/ninja%3A%2F%2Ftest_consistent_failure/results/one",
  1005  					ResultId:    "one",
  1006  					Expected:    false,
  1007  					Status:      pb.TestResultStatus_FAIL,
  1008  					SummaryHtml: "SummaryHTML",
  1009  					StartTime:   timestamppb.New(time.Date(2010, time.March, 1, 0, 0, 0, 0, time.UTC)),
  1010  					Duration:    3.000001,
  1011  					FailureReason: &pb.FailureReason{
  1012  						PrimaryErrorMessage: "abc.def(123): unexpected nil-deference",
  1013  					},
  1014  					Properties: expectedProperties,
  1015  				},
  1016  			},
  1017  			Exonerations: []*bqpb.TestVerdictRow_Exoneration{
  1018  				{
  1019  					ExplanationHtml: "LUCI Analysis reported this test as flaky.",
  1020  					Reason:          pb.ExonerationReason_OCCURS_ON_OTHER_CLS,
  1021  				},
  1022  				{
  1023  					ExplanationHtml: "Test is marked informational.",
  1024  					Reason:          pb.ExonerationReason_NOT_CRITICAL,
  1025  				},
  1026  				{
  1027  					Reason: pb.ExonerationReason_OCCURS_ON_MAINLINE,
  1028  				},
  1029  			},
  1030  			Counts: &bqpb.TestVerdictRow_Counts{
  1031  				Total: 1, TotalNonSkipped: 1, Unexpected: 1, UnexpectedNonSkipped: 1, UnexpectedNonSkippedNonPassed: 1,
  1032  			},
  1033  			BuildbucketBuild:  buildbucketBuild,
  1034  			ChangeVerifierRun: cvRun,
  1035  			Sources: &pb.Sources{
  1036  				GitilesCommit: &pb.GitilesCommit{
  1037  					Host:       "project.googlesource.com",
  1038  					Project:    "myproject/src",
  1039  					Ref:        "refs/heads/main",
  1040  					CommitHash: "abcdefabcd1234567890abcdefabcd1234567890",
  1041  					Position:   16801,
  1042  				},
  1043  				Changelists: []*pb.GerritChange{
  1044  					{
  1045  						Host:      "project-review.googlesource.com",
  1046  						Project:   "myproject/src2",
  1047  						Change:    9991,
  1048  						Patchset:  82,
  1049  						OwnerKind: pb.ChangelistOwnerKind_HUMAN,
  1050  					},
  1051  				},
  1052  				IsDirty: false,
  1053  			},
  1054  			SourceRef:     sr,
  1055  			SourceRefHash: hex.EncodeToString(pbutil.SourceRefHash(sr)),
  1056  		},
  1057  		{
  1058  			Project:       "project",
  1059  			TestId:        "ninja://test_expected",
  1060  			Variant:       "{}",
  1061  			VariantHash:   "hash",
  1062  			Invocation:    invocation,
  1063  			PartitionTime: timestamppb.New(expectedPartitionTime),
  1064  			Status:        pb.TestVerdictStatus_EXPECTED,
  1065  			Results: []*bqpb.TestVerdictRow_TestResult{
  1066  				{
  1067  					Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{
  1068  						Id: "build-1234",
  1069  					},
  1070  					Name:       "invocations/build-1234/tests/ninja%3A%2F%2Ftest_expected/results/one",
  1071  					ResultId:   "one",
  1072  					StartTime:  timestamppb.New(time.Date(2010, time.May, 1, 0, 0, 0, 0, time.UTC)),
  1073  					Status:     pb.TestResultStatus_PASS,
  1074  					Expected:   true,
  1075  					Duration:   5.0,
  1076  					Properties: "{}",
  1077  				},
  1078  			},
  1079  			Counts: &bqpb.TestVerdictRow_Counts{
  1080  				Total: 1, TotalNonSkipped: 1, Unexpected: 0, UnexpectedNonSkipped: 0, UnexpectedNonSkippedNonPassed: 0,
  1081  			},
  1082  			BuildbucketBuild:  buildbucketBuild,
  1083  			ChangeVerifierRun: cvRun,
  1084  		},
  1085  		{
  1086  			Project:       "project",
  1087  			TestId:        "ninja://test_filtering_event",
  1088  			Variant:       "{}",
  1089  			VariantHash:   "hash",
  1090  			Invocation:    invocation,
  1091  			PartitionTime: timestamppb.New(expectedPartitionTime),
  1092  			Status:        pb.TestVerdictStatus_EXPECTED,
  1093  			Results: []*bqpb.TestVerdictRow_TestResult{
  1094  				{
  1095  					Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{
  1096  						Id: "build-1234",
  1097  					},
  1098  					Name:       "invocations/build-1234/tests/ninja%3A%2F%2Ftest_filtering_event/results/one",
  1099  					ResultId:   "one",
  1100  					StartTime:  timestamppb.New(time.Date(2010, time.February, 2, 0, 0, 0, 0, time.UTC)),
  1101  					Status:     pb.TestResultStatus_SKIP,
  1102  					SkipReason: pb.SkipReason_AUTOMATICALLY_DISABLED_FOR_FLAKINESS.String(),
  1103  					Expected:   true,
  1104  					Properties: "{}",
  1105  				},
  1106  			},
  1107  			Counts: &bqpb.TestVerdictRow_Counts{
  1108  				Total: 1, TotalNonSkipped: 0, Unexpected: 0, UnexpectedNonSkipped: 0, UnexpectedNonSkippedNonPassed: 0,
  1109  			},
  1110  			BuildbucketBuild:  buildbucketBuild,
  1111  			ChangeVerifierRun: cvRun,
  1112  		},
  1113  		{
  1114  			Project:       "project",
  1115  			TestId:        "ninja://test_from_luci_bisection",
  1116  			Variant:       "{}",
  1117  			VariantHash:   "hash",
  1118  			Invocation:    invocation,
  1119  			PartitionTime: timestamppb.New(expectedPartitionTime),
  1120  			Status:        pb.TestVerdictStatus_UNEXPECTED,
  1121  			Results: []*bqpb.TestVerdictRow_TestResult{
  1122  				{
  1123  					Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{
  1124  						Id: "build-1234",
  1125  					},
  1126  					Name:       "invocations/build-1234/tests/ninja%3A%2F%2Ftest_from_luci_bisection/results/one",
  1127  					ResultId:   "one",
  1128  					Status:     pb.TestResultStatus_PASS,
  1129  					Expected:   false,
  1130  					Properties: "{}",
  1131  					Tags:       []*pb.StringPair{{Key: "is_luci_bisection", Value: "true"}},
  1132  				},
  1133  			},
  1134  			Counts: &bqpb.TestVerdictRow_Counts{
  1135  				Total: 1, TotalNonSkipped: 1, Unexpected: 1, UnexpectedNonSkipped: 1, UnexpectedNonSkippedNonPassed: 0,
  1136  			},
  1137  			BuildbucketBuild:  buildbucketBuild,
  1138  			ChangeVerifierRun: cvRun,
  1139  		},
  1140  		{
  1141  			Project:       "project",
  1142  			TestId:        "ninja://test_has_unexpected",
  1143  			VariantHash:   "hash",
  1144  			Variant:       "{}",
  1145  			Invocation:    invocation,
  1146  			PartitionTime: timestamppb.New(expectedPartitionTime),
  1147  			Status:        pb.TestVerdictStatus_FLAKY,
  1148  			Results: []*bqpb.TestVerdictRow_TestResult{
  1149  				{
  1150  					Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{
  1151  						Id: "invocation-0b",
  1152  					},
  1153  					Name:       "invocations/invocation-0b/tests/ninja%3A%2F%2Ftest_has_unexpected/results/one",
  1154  					ResultId:   "one",
  1155  					StartTime:  timestamppb.New(time.Date(2010, time.February, 1, 0, 0, 10, 0, time.UTC)),
  1156  					Status:     pb.TestResultStatus_FAIL,
  1157  					Expected:   false,
  1158  					Properties: "{}",
  1159  				},
  1160  				{
  1161  					Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{
  1162  						Id: "invocation-0a",
  1163  					},
  1164  					Name:       "invocations/invocation-0a/tests/ninja%3A%2F%2Ftest_has_unexpected/results/two",
  1165  					ResultId:   "two",
  1166  					StartTime:  timestamppb.New(time.Date(2010, time.February, 1, 0, 0, 20, 0, time.UTC)),
  1167  					Status:     pb.TestResultStatus_PASS,
  1168  					Expected:   true,
  1169  					Properties: "{}",
  1170  				},
  1171  			},
  1172  			Counts: &bqpb.TestVerdictRow_Counts{
  1173  				Total: 2, TotalNonSkipped: 2, Unexpected: 1, UnexpectedNonSkipped: 1, UnexpectedNonSkippedNonPassed: 1,
  1174  			},
  1175  			BuildbucketBuild:  buildbucketBuild,
  1176  			ChangeVerifierRun: cvRun,
  1177  		},
  1178  		{
  1179  			Project:       "project",
  1180  			TestId:        "ninja://test_known_flake",
  1181  			Variant:       `{"k1":"v2"}`,
  1182  			VariantHash:   "hash_2",
  1183  			Invocation:    invocation,
  1184  			PartitionTime: timestamppb.New(expectedPartitionTime),
  1185  			Status:        pb.TestVerdictStatus_UNEXPECTED,
  1186  			TestMetadata:  testMetadata,
  1187  			Results: []*bqpb.TestVerdictRow_TestResult{
  1188  				{
  1189  					Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{
  1190  						Id: "build-1234",
  1191  					},
  1192  					Name:       "invocations/build-1234/tests/ninja%3A%2F%2Ftest_known_flake/results/one",
  1193  					ResultId:   "one",
  1194  					StartTime:  timestamppb.New(time.Date(2010, time.February, 1, 0, 0, 0, 0, time.UTC)),
  1195  					Status:     pb.TestResultStatus_FAIL,
  1196  					Expected:   false,
  1197  					Duration:   2.0,
  1198  					Tags:       pbutil.StringPairs("os", "Mac", "monorail_component", "Monorail>Component"),
  1199  					Properties: "{}",
  1200  				},
  1201  			},
  1202  			Counts: &bqpb.TestVerdictRow_Counts{
  1203  				Total: 1, TotalNonSkipped: 1, Unexpected: 1, UnexpectedNonSkipped: 1, UnexpectedNonSkippedNonPassed: 1,
  1204  			},
  1205  			BuildbucketBuild:  buildbucketBuild,
  1206  			ChangeVerifierRun: cvRun,
  1207  		},
  1208  		{
  1209  			Project:       "project",
  1210  			TestId:        "ninja://test_new_failure",
  1211  			Variant:       `{"k1":"v1"}`,
  1212  			VariantHash:   "hash_1",
  1213  			Invocation:    invocation,
  1214  			PartitionTime: timestamppb.New(expectedPartitionTime),
  1215  			Status:        pb.TestVerdictStatus_UNEXPECTED,
  1216  			TestMetadata:  testMetadata,
  1217  			Results: []*bqpb.TestVerdictRow_TestResult{
  1218  				{
  1219  					Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{
  1220  						Id: "build-1234",
  1221  					},
  1222  					Name:       "invocations/build-1234/tests/ninja%3A%2F%2Ftest_new_failure/results/one",
  1223  					ResultId:   "one",
  1224  					StartTime:  timestamppb.New(time.Date(2010, time.January, 1, 0, 0, 0, 0, time.UTC)),
  1225  					Status:     pb.TestResultStatus_FAIL,
  1226  					Expected:   false,
  1227  					Duration:   1.0,
  1228  					Tags:       pbutil.StringPairs("random_tag", "random_tag_value", "public_buganizer_component", "951951951"),
  1229  					Properties: "{}",
  1230  				},
  1231  			},
  1232  			Counts: &bqpb.TestVerdictRow_Counts{
  1233  				Total: 1, TotalNonSkipped: 1, Unexpected: 1, UnexpectedNonSkipped: 1, UnexpectedNonSkippedNonPassed: 1,
  1234  			},
  1235  			BuildbucketBuild:  buildbucketBuild,
  1236  			ChangeVerifierRun: cvRun,
  1237  		},
  1238  		{
  1239  			Project:       "project",
  1240  			TestId:        "ninja://test_new_flake",
  1241  			Variant:       "{}",
  1242  			VariantHash:   "hash",
  1243  			Invocation:    invocation,
  1244  			PartitionTime: timestamppb.New(expectedPartitionTime),
  1245  			Status:        pb.TestVerdictStatus_FLAKY,
  1246  			Results: []*bqpb.TestVerdictRow_TestResult{
  1247  				{
  1248  					Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{
  1249  						Id: "invocation-1234",
  1250  					},
  1251  					Name:       "invocations/invocation-1234/tests/ninja%3A%2F%2Ftest_new_flake/results/two",
  1252  					ResultId:   "two",
  1253  					StartTime:  timestamppb.New(time.Date(2010, time.January, 1, 0, 0, 20, 0, time.UTC)),
  1254  					Status:     pb.TestResultStatus_FAIL,
  1255  					Expected:   false,
  1256  					Duration:   11.0,
  1257  					Properties: "{}",
  1258  				},
  1259  				{
  1260  					Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{
  1261  						Id: "invocation-1234",
  1262  					},
  1263  					Name:       "invocations/invocation-1234/tests/ninja%3A%2F%2Ftest_new_flake/results/one",
  1264  					ResultId:   "one",
  1265  					StartTime:  timestamppb.New(time.Date(2010, time.January, 1, 0, 0, 10, 0, time.UTC)),
  1266  					Status:     pb.TestResultStatus_FAIL,
  1267  					Expected:   false,
  1268  					Duration:   10.0,
  1269  					Properties: "{}",
  1270  				},
  1271  				{
  1272  					Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{
  1273  						Id: "invocation-4567",
  1274  					},
  1275  					Name:       "invocations/invocation-4567/tests/ninja%3A%2F%2Ftest_new_flake/results/three",
  1276  					ResultId:   "three",
  1277  					StartTime:  timestamppb.New(time.Date(2010, time.January, 1, 0, 0, 15, 0, time.UTC)),
  1278  					Status:     pb.TestResultStatus_PASS,
  1279  					Expected:   true,
  1280  					Duration:   12.0,
  1281  					Properties: "{}",
  1282  				},
  1283  			},
  1284  			Counts: &bqpb.TestVerdictRow_Counts{
  1285  				Total: 3, TotalNonSkipped: 3, Unexpected: 2, UnexpectedNonSkipped: 2, UnexpectedNonSkippedNonPassed: 2,
  1286  			},
  1287  			BuildbucketBuild:  buildbucketBuild,
  1288  			ChangeVerifierRun: cvRun,
  1289  		},
  1290  		{
  1291  			Project:       "project",
  1292  			TestId:        "ninja://test_no_new_results",
  1293  			Variant:       "{}",
  1294  			VariantHash:   "hash",
  1295  			Invocation:    invocation,
  1296  			PartitionTime: timestamppb.New(expectedPartitionTime),
  1297  			Status:        pb.TestVerdictStatus_UNEXPECTED,
  1298  			Results: []*bqpb.TestVerdictRow_TestResult{
  1299  				{
  1300  					Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{
  1301  						Id: "build-1234",
  1302  					},
  1303  					Name:       "invocations/build-1234/tests/ninja%3A%2F%2Ftest_no_new_results/results/one",
  1304  					ResultId:   "one",
  1305  					StartTime:  timestamppb.New(time.Date(2010, time.April, 1, 0, 0, 0, 0, time.UTC)),
  1306  					Status:     pb.TestResultStatus_FAIL,
  1307  					Expected:   false,
  1308  					Duration:   4.0,
  1309  					Properties: "{}",
  1310  				},
  1311  			},
  1312  			Counts: &bqpb.TestVerdictRow_Counts{
  1313  				Total: 1, TotalNonSkipped: 1, Unexpected: 1, UnexpectedNonSkipped: 1, UnexpectedNonSkippedNonPassed: 1,
  1314  			},
  1315  			BuildbucketBuild:  buildbucketBuild,
  1316  			ChangeVerifierRun: cvRun,
  1317  		},
  1318  		{
  1319  			Project:       "project",
  1320  			TestId:        "ninja://test_skip",
  1321  			Variant:       "{}",
  1322  			VariantHash:   "hash",
  1323  			Invocation:    invocation,
  1324  			PartitionTime: timestamppb.New(expectedPartitionTime),
  1325  			Status:        pb.TestVerdictStatus_UNEXPECTEDLY_SKIPPED,
  1326  			Results: []*bqpb.TestVerdictRow_TestResult{
  1327  				{
  1328  					Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{
  1329  						Id: "build-1234",
  1330  					},
  1331  					Name:       "invocations/build-1234/tests/ninja%3A%2F%2Ftest_skip/results/one",
  1332  					ResultId:   "one",
  1333  					StartTime:  timestamppb.New(time.Date(2010, time.February, 2, 0, 0, 0, 0, time.UTC)),
  1334  					Status:     pb.TestResultStatus_SKIP,
  1335  					Expected:   false,
  1336  					Properties: "{}",
  1337  				},
  1338  			},
  1339  			Counts: &bqpb.TestVerdictRow_Counts{
  1340  				Total: 1, TotalNonSkipped: 0, Unexpected: 1, UnexpectedNonSkipped: 0, UnexpectedNonSkippedNonPassed: 0,
  1341  			},
  1342  			BuildbucketBuild:  buildbucketBuild,
  1343  			ChangeVerifierRun: cvRun,
  1344  		},
  1345  		{
  1346  			Project:       "project",
  1347  			TestId:        "ninja://test_unexpected_pass",
  1348  			Variant:       "{}",
  1349  			VariantHash:   "hash",
  1350  			Invocation:    invocation,
  1351  			PartitionTime: timestamppb.New(expectedPartitionTime),
  1352  			Status:        pb.TestVerdictStatus_UNEXPECTED,
  1353  			Results: []*bqpb.TestVerdictRow_TestResult{
  1354  				{
  1355  					Parent: &bqpb.TestVerdictRow_ParentInvocationRecord{
  1356  						Id: "build-1234",
  1357  					},
  1358  					Name:       "invocations/build-1234/tests/ninja%3A%2F%2Ftest_unexpected_pass/results/one",
  1359  					ResultId:   "one",
  1360  					Status:     pb.TestResultStatus_PASS,
  1361  					Expected:   false,
  1362  					Properties: "{}",
  1363  				},
  1364  			},
  1365  			Counts: &bqpb.TestVerdictRow_Counts{
  1366  				Total: 1, TotalNonSkipped: 1, Unexpected: 1, UnexpectedNonSkipped: 1, UnexpectedNonSkippedNonPassed: 0,
  1367  			},
  1368  			BuildbucketBuild:  buildbucketBuild,
  1369  			ChangeVerifierRun: cvRun,
  1370  		},
  1371  	}
  1372  	So(actualRows, ShouldHaveLength, len(expectedRows))
  1373  	for i, row := range actualRows {
  1374  		So(row, ShouldResembleProto, expectedRows[i])
  1375  	}
  1376  }
  1377  
  1378  func verifyContinuationTask(skdr *tqtesting.Scheduler, expectedContinuation *taskspb.IngestTestResults) {
  1379  	count := 0
  1380  	for _, pl := range skdr.Tasks().Payloads() {
  1381  		if pl, ok := pl.(*taskspb.IngestTestResults); ok {
  1382  			So(pl, ShouldResembleProto, expectedContinuation)
  1383  			count++
  1384  		}
  1385  	}
  1386  	if expectedContinuation != nil {
  1387  		So(count, ShouldEqual, 1)
  1388  	} else {
  1389  		So(count, ShouldEqual, 0)
  1390  	}
  1391  }
  1392  
  1393  func verifyIngestionControl(ctx context.Context, expected *control.Entry) {
  1394  	actual, err := control.Read(span.Single(ctx), []string{expected.BuildID})
  1395  	So(err, ShouldBeNil)
  1396  	So(actual, ShouldHaveLength, 1)
  1397  	a := *actual[0]
  1398  	e := *expected
  1399  
  1400  	// Compare protos separately, as they are not compared
  1401  	// correctly by ShouldResemble.
  1402  	So(a.PresubmitResult, ShouldResembleProto, e.PresubmitResult)
  1403  	a.PresubmitResult = nil
  1404  	e.PresubmitResult = nil
  1405  
  1406  	So(a.BuildResult, ShouldResembleProto, e.BuildResult)
  1407  	a.BuildResult = nil
  1408  	e.BuildResult = nil
  1409  
  1410  	So(a.InvocationResult, ShouldResembleProto, e.InvocationResult)
  1411  	a.InvocationResult = nil
  1412  	e.InvocationResult = nil
  1413  
  1414  	// Do not compare last updated time, as it is determined
  1415  	// by commit timestamp.
  1416  	So(a.LastUpdated, ShouldNotBeEmpty)
  1417  	e.LastUpdated = a.LastUpdated
  1418  
  1419  	So(a, ShouldResemble, e)
  1420  }