go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/changepoints/analyze_changepoints_test.go (about)

     1  // Copyright 2023 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 changepoints
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"sort"
    21  	"testing"
    22  	"time"
    23  
    24  	"cloud.google.com/go/spanner"
    25  	"google.golang.org/protobuf/types/known/timestamppb"
    26  
    27  	"go.chromium.org/luci/gae/impl/memory"
    28  	rdbpb "go.chromium.org/luci/resultdb/proto/v1"
    29  	"go.chromium.org/luci/server/span"
    30  
    31  	"go.chromium.org/luci/analysis/internal/changepoints/bqexporter"
    32  	"go.chromium.org/luci/analysis/internal/changepoints/inputbuffer"
    33  	cpb "go.chromium.org/luci/analysis/internal/changepoints/proto"
    34  	tu "go.chromium.org/luci/analysis/internal/changepoints/testutil"
    35  	"go.chromium.org/luci/analysis/internal/changepoints/testvariantbranch"
    36  	"go.chromium.org/luci/analysis/internal/config"
    37  	controlpb "go.chromium.org/luci/analysis/internal/ingestion/control/proto"
    38  	spanutil "go.chromium.org/luci/analysis/internal/span"
    39  	"go.chromium.org/luci/analysis/internal/tasks/taskspb"
    40  	"go.chromium.org/luci/analysis/internal/testutil"
    41  	"go.chromium.org/luci/analysis/pbutil"
    42  	bqpb "go.chromium.org/luci/analysis/proto/bq"
    43  	pb "go.chromium.org/luci/analysis/proto/v1"
    44  
    45  	. "github.com/smartystreets/goconvey/convey"
    46  	. "go.chromium.org/luci/common/testing/assertions"
    47  )
    48  
    49  type Invocation struct {
    50  	Project              string
    51  	InvocationID         string
    52  	IngestedInvocationID string
    53  }
    54  
    55  func TestAnalyzeChangePoint(t *testing.T) {
    56  	exporter, _ := fakeExporter()
    57  	Convey(`Can batch result`, t, func() {
    58  		ctx := newContext(t)
    59  		payload := tu.SamplePayload()
    60  		sourcesMap := tu.SampleSourcesMap(10)
    61  		// 900 test variants should result in 5 batches (1000 each, last one has 500).
    62  		tvs := testVariants(4500)
    63  		err := Analyze(ctx, tvs, payload, sourcesMap, exporter)
    64  		So(err, ShouldBeNil)
    65  
    66  		// Check that there are 5 checkpoints created.
    67  		So(countCheckPoint(ctx), ShouldEqual, 5)
    68  	})
    69  
    70  	Convey(`Can skip batch`, t, func() {
    71  		ctx := newContext(t)
    72  		payload := tu.SamplePayload()
    73  		sourcesMap := tu.SampleSourcesMap(10)
    74  		tvs := testVariants(100)
    75  		err := analyzeSingleBatch(ctx, tvs, payload, sourcesMap, exporter)
    76  		So(err, ShouldBeNil)
    77  		So(countCheckPoint(ctx), ShouldEqual, 1)
    78  
    79  		// Analyze the batch again should not throw an error.
    80  		err = analyzeSingleBatch(ctx, tvs, payload, sourcesMap, exporter)
    81  		So(err, ShouldBeNil)
    82  		So(countCheckPoint(ctx), ShouldEqual, 1)
    83  	})
    84  
    85  	Convey(`No commit position should skip`, t, func() {
    86  		ctx := newContext(t)
    87  		payload := tu.SamplePayload()
    88  		sourcesMap := map[string]*pb.Sources{
    89  			"sources_id": {
    90  				GitilesCommit: &pb.GitilesCommit{
    91  					Host:    "host",
    92  					Project: "proj",
    93  					Ref:     "ref",
    94  				},
    95  			},
    96  		}
    97  		tvs := testVariants(100)
    98  		err := Analyze(ctx, tvs, payload, sourcesMap, exporter)
    99  		So(err, ShouldBeNil)
   100  		So(countCheckPoint(ctx), ShouldEqual, 0)
   101  		So(verdictCounter.Get(ctx, "chromium", "skipped_no_commit_data"), ShouldEqual, 100)
   102  	})
   103  
   104  	Convey(`Filter test variant`, t, func() {
   105  		ctx := newContext(t)
   106  		payload := &taskspb.IngestTestResults{
   107  			Build: &controlpb.BuildResult{
   108  				Project: "chromium",
   109  			},
   110  		}
   111  
   112  		sourcesMap := map[string]*pb.Sources{
   113  			"sources_id": {
   114  				GitilesCommit: &pb.GitilesCommit{
   115  					Host:     "host",
   116  					Project:  "proj",
   117  					Ref:      "ref",
   118  					Position: 10,
   119  				},
   120  			},
   121  			"sources_id_2": {
   122  				GitilesCommit: &pb.GitilesCommit{
   123  					Host:     "host_2",
   124  					Project:  "proj_2",
   125  					Ref:      "ref_2",
   126  					Position: 10,
   127  				},
   128  				IsDirty: true,
   129  			},
   130  		}
   131  		tvs := []*rdbpb.TestVariant{
   132  			{
   133  				// All skip.
   134  				TestId: "1",
   135  				Results: []*rdbpb.TestResultBundle{
   136  					{
   137  						Result: &rdbpb.TestResult{
   138  							Name:   "invocations/inv-1/tests/abc",
   139  							Status: rdbpb.TestStatus_SKIP,
   140  						},
   141  					},
   142  				},
   143  				SourcesId: "sources_id",
   144  			},
   145  			{
   146  				// Duplicate.
   147  				TestId: "2",
   148  				Results: []*rdbpb.TestResultBundle{
   149  					{
   150  						Result: &rdbpb.TestResult{
   151  							Name:   "invocations/inv-2/tests/abc",
   152  							Status: rdbpb.TestStatus_PASS,
   153  						},
   154  					},
   155  					{
   156  						Result: &rdbpb.TestResult{
   157  							Name:   "invocations/inv-2/tests/abc",
   158  							Status: rdbpb.TestStatus_FAIL,
   159  						},
   160  					},
   161  				},
   162  				SourcesId: "sources_id",
   163  			},
   164  			{
   165  				// OK.
   166  				TestId: "3",
   167  				Results: []*rdbpb.TestResultBundle{
   168  					{
   169  						Result: &rdbpb.TestResult{
   170  							Name:   "invocations/inv-3/tests/abc",
   171  							Status: rdbpb.TestStatus_PASS,
   172  						},
   173  					},
   174  				},
   175  				SourcesId: "sources_id",
   176  			},
   177  			{
   178  				// No source ID.
   179  				TestId: "4",
   180  				Results: []*rdbpb.TestResultBundle{
   181  					{
   182  						Result: &rdbpb.TestResult{
   183  							Name:   "invocations/inv-4/tests/abc",
   184  							Status: rdbpb.TestStatus_PASS,
   185  						},
   186  					},
   187  				},
   188  				SourcesId: "sources_id_1",
   189  			},
   190  			{
   191  				// Source is dirty.
   192  				TestId: "5",
   193  				Results: []*rdbpb.TestResultBundle{
   194  					{
   195  						Result: &rdbpb.TestResult{
   196  							Name:   "invocations/inv-5/tests/abc",
   197  							Status: rdbpb.TestStatus_PASS,
   198  						},
   199  					},
   200  				},
   201  				SourcesId: "sources_id_2",
   202  			},
   203  		}
   204  		duplicateMap := map[string]bool{
   205  			"inv-2": true,
   206  		}
   207  		tvs, err := filterTestVariants(ctx, tvs, payload, duplicateMap, sourcesMap)
   208  		So(err, ShouldBeNil)
   209  		So(len(tvs), ShouldEqual, 1)
   210  		So(tvs[0].TestId, ShouldEqual, "3")
   211  		So(verdictCounter.Get(ctx, "chromium", "skipped_no_source"), ShouldEqual, 1)
   212  		So(verdictCounter.Get(ctx, "chromium", "skipped_no_commit_data"), ShouldEqual, 1)
   213  		So(verdictCounter.Get(ctx, "chromium", "skipped_all_skipped_or_duplicate"), ShouldEqual, 2)
   214  	})
   215  
   216  	Convey(`Filter test variant with failed presubmit`, t, func() {
   217  		ctx := newContext(t)
   218  		payload := &taskspb.IngestTestResults{
   219  			Build: &controlpb.BuildResult{
   220  				Project: "chromium",
   221  			},
   222  			PresubmitRun: &controlpb.PresubmitResult{
   223  				Status: pb.PresubmitRunStatus_PRESUBMIT_RUN_STATUS_FAILED,
   224  				Mode:   pb.PresubmitRunMode_FULL_RUN,
   225  			},
   226  		}
   227  
   228  		sourcesMap := tu.SampleSourcesMap(10)
   229  		sourcesMap["sources_id"].Changelists = []*pb.GerritChange{
   230  			{
   231  				Host:     "host",
   232  				Project:  "proj",
   233  				Patchset: 1,
   234  				Change:   12345,
   235  			},
   236  		}
   237  		tvs := []*rdbpb.TestVariant{
   238  			{
   239  				TestId: "1",
   240  				Results: []*rdbpb.TestResultBundle{
   241  					{
   242  						Result: &rdbpb.TestResult{
   243  							Name:   "invocations/inv-1/tests/abc",
   244  							Status: rdbpb.TestStatus_PASS,
   245  						},
   246  					},
   247  				},
   248  				SourcesId: "sources_id",
   249  			},
   250  		}
   251  		duplicateMap := map[string]bool{}
   252  		tvs, err := filterTestVariants(ctx, tvs, payload, duplicateMap, sourcesMap)
   253  		So(err, ShouldBeNil)
   254  		So(len(tvs), ShouldEqual, 0)
   255  		So(verdictCounter.Get(ctx, "chromium", "skipped_unsubmitted_code"), ShouldEqual, 1)
   256  	})
   257  }
   258  
   259  func TestAnalyzeSingleBatch(t *testing.T) {
   260  	exporter, client := fakeExporter()
   261  	Convey(`Analyze batch with empty buffer`, t, func() {
   262  		ctx := newContext(t)
   263  		payload := tu.SamplePayload()
   264  		sourcesMap := tu.SampleSourcesMap(10)
   265  		tvs := []*rdbpb.TestVariant{
   266  			{
   267  				TestId:      "test_1",
   268  				VariantHash: "hash_1",
   269  				Variant: &rdbpb.Variant{
   270  					Def: map[string]string{
   271  						"k": "v",
   272  					},
   273  				},
   274  				Status: rdbpb.TestVariantStatus_EXPECTED,
   275  				Results: []*rdbpb.TestResultBundle{
   276  					{
   277  						Result: &rdbpb.TestResult{
   278  							Name:     "invocations/abc/tests/xyz",
   279  							Status:   rdbpb.TestStatus_PASS,
   280  							Expected: true,
   281  						},
   282  					},
   283  				},
   284  				SourcesId: "sources_id",
   285  			},
   286  			{
   287  				TestId:      "test_2",
   288  				VariantHash: "hash_2",
   289  				Variant: &rdbpb.Variant{
   290  					Def: map[string]string{
   291  						"k2": "v2",
   292  					},
   293  				},
   294  				Status: rdbpb.TestVariantStatus_UNEXPECTED,
   295  				Results: []*rdbpb.TestResultBundle{
   296  					{
   297  						Result: &rdbpb.TestResult{
   298  							Name:     "invocations/def/tests/xyz",
   299  							Status:   rdbpb.TestStatus_PASS,
   300  							Expected: true,
   301  						},
   302  					},
   303  					{
   304  						Result: &rdbpb.TestResult{
   305  							Name:     "invocations/def/tests/xyz",
   306  							Status:   rdbpb.TestStatus_FAIL,
   307  							Expected: true,
   308  						},
   309  					},
   310  					{
   311  						Result: &rdbpb.TestResult{
   312  							Name:     "invocations/def/tests/xyz",
   313  							Status:   rdbpb.TestStatus_CRASH,
   314  							Expected: true,
   315  						},
   316  					},
   317  					{
   318  						Result: &rdbpb.TestResult{
   319  							Name:     "invocations/def/tests/xyz",
   320  							Status:   rdbpb.TestStatus_ABORT,
   321  							Expected: true,
   322  						},
   323  					},
   324  					{
   325  						Result: &rdbpb.TestResult{
   326  							Name:   "invocations/def/tests/xyz",
   327  							Status: rdbpb.TestStatus_PASS,
   328  						},
   329  					},
   330  					{
   331  						Result: &rdbpb.TestResult{
   332  							Name:   "invocations/def/tests/xyz",
   333  							Status: rdbpb.TestStatus_FAIL,
   334  						},
   335  					},
   336  					{
   337  						Result: &rdbpb.TestResult{
   338  							Name:   "invocations/def/tests/xyz",
   339  							Status: rdbpb.TestStatus_CRASH,
   340  						},
   341  					},
   342  					{
   343  						Result: &rdbpb.TestResult{
   344  							Name:   "invocations/def/tests/xyz",
   345  							Status: rdbpb.TestStatus_ABORT,
   346  						},
   347  					},
   348  				},
   349  				SourcesId: "sources_id",
   350  			},
   351  		}
   352  
   353  		err := analyzeSingleBatch(ctx, tvs, payload, sourcesMap, exporter)
   354  		So(err, ShouldBeNil)
   355  		So(countCheckPoint(ctx), ShouldEqual, 1)
   356  
   357  		// Check invocations.
   358  		invs := fetchInvocations(ctx)
   359  		So(invs, ShouldResemble, []Invocation{
   360  			{
   361  				Project:              "chromium",
   362  				InvocationID:         "abc",
   363  				IngestedInvocationID: "build-1234",
   364  			},
   365  			{
   366  				Project:              "chromium",
   367  				InvocationID:         "def",
   368  				IngestedInvocationID: "build-1234",
   369  			},
   370  		})
   371  
   372  		// Check test variant branch.
   373  		tvbs, err := FetchTestVariantBranches(ctx)
   374  		So(err, ShouldBeNil)
   375  		So(len(tvbs), ShouldEqual, 2)
   376  
   377  		So(tvbs[0], ShouldResembleProto, &testvariantbranch.Entry{
   378  			Project:     "chromium",
   379  			TestID:      "test_1",
   380  			VariantHash: "hash_1",
   381  			RefHash:     pbutil.SourceRefHash(pbutil.SourceRefFromSources(sourcesMap["sources_id"])),
   382  			Variant: &pb.Variant{
   383  				Def: map[string]string{
   384  					"k": "v",
   385  				},
   386  			},
   387  			SourceRef: &pb.SourceRef{
   388  				System: &pb.SourceRef_Gitiles{
   389  					Gitiles: &pb.GitilesRef{
   390  						Host:    "host",
   391  						Project: "proj",
   392  						Ref:     "ref",
   393  					},
   394  				},
   395  			},
   396  			InputBuffer: &inputbuffer.Buffer{
   397  				HotBuffer: inputbuffer.History{
   398  					Verdicts: []inputbuffer.PositionVerdict{
   399  						{
   400  							CommitPosition:       10,
   401  							IsSimpleExpectedPass: true,
   402  							Hour:                 payload.PartitionTime.AsTime(),
   403  						},
   404  					},
   405  				},
   406  				ColdBuffer: inputbuffer.History{
   407  					Verdicts: []inputbuffer.PositionVerdict{},
   408  				},
   409  				HotBufferCapacity:  inputbuffer.DefaultHotBufferCapacity,
   410  				ColdBufferCapacity: inputbuffer.DefaultColdBufferCapacity,
   411  			},
   412  		})
   413  
   414  		So(tvbs[1], ShouldResembleProto, &testvariantbranch.Entry{
   415  			Project:     "chromium",
   416  			TestID:      "test_2",
   417  			VariantHash: "hash_2",
   418  			RefHash:     pbutil.SourceRefHash(pbutil.SourceRefFromSources(sourcesMap["sources_id"])),
   419  			Variant: &pb.Variant{
   420  				Def: map[string]string{
   421  					"k2": "v2",
   422  				},
   423  			},
   424  			SourceRef: &pb.SourceRef{
   425  				System: &pb.SourceRef_Gitiles{
   426  					Gitiles: &pb.GitilesRef{
   427  						Host:    "host",
   428  						Project: "proj",
   429  						Ref:     "ref",
   430  					},
   431  				},
   432  			},
   433  			InputBuffer: &inputbuffer.Buffer{
   434  				HotBuffer: inputbuffer.History{
   435  					Verdicts: []inputbuffer.PositionVerdict{
   436  						{
   437  							CommitPosition:       10,
   438  							IsSimpleExpectedPass: false,
   439  							Hour:                 payload.PartitionTime.AsTime(),
   440  							Details: inputbuffer.VerdictDetails{
   441  								IsExonerated: false,
   442  								Runs: []inputbuffer.Run{
   443  									{
   444  										Expected: inputbuffer.ResultCounts{
   445  											PassCount:  1,
   446  											FailCount:  1,
   447  											CrashCount: 1,
   448  											AbortCount: 1,
   449  										},
   450  										Unexpected: inputbuffer.ResultCounts{
   451  											PassCount:  1,
   452  											FailCount:  1,
   453  											CrashCount: 1,
   454  											AbortCount: 1,
   455  										},
   456  									},
   457  								},
   458  							},
   459  						},
   460  					},
   461  				},
   462  				ColdBuffer: inputbuffer.History{
   463  					Verdicts: []inputbuffer.PositionVerdict{},
   464  				},
   465  				HotBufferCapacity:  inputbuffer.DefaultHotBufferCapacity,
   466  				ColdBufferCapacity: inputbuffer.DefaultColdBufferCapacity,
   467  			},
   468  		})
   469  
   470  		So(len(client.Insertions), ShouldEqual, 2)
   471  		for _, insert := range client.Insertions {
   472  			// Check the version is populated, but do not assert its value
   473  			// as it is a commit timestamp that varies each time the test runs.
   474  			So(insert.Version, ShouldNotBeNil)
   475  			insert.Version = nil
   476  		}
   477  
   478  		sort.Slice(client.Insertions, func(i, j int) bool {
   479  			return client.Insertions[i].TestId < client.Insertions[j].TestId
   480  		})
   481  		So(client.Insertions[0], ShouldResembleProto, &bqpb.TestVariantBranchRow{
   482  			Project:     "chromium",
   483  			TestId:      "test_1",
   484  			VariantHash: "hash_1",
   485  			RefHash:     "6de221242e011c91",
   486  			Variant:     "{\"k\":\"v\"}",
   487  			Ref: &pb.SourceRef{
   488  				System: &pb.SourceRef_Gitiles{
   489  					Gitiles: &pb.GitilesRef{
   490  						Host:    "host",
   491  						Project: "proj",
   492  						Ref:     "ref",
   493  					},
   494  				},
   495  			},
   496  			Segments: []*bqpb.Segment{
   497  				{
   498  					StartPosition: 10,
   499  					StartHour:     timestamppb.New(time.Unix(3600*10, 0)),
   500  					EndPosition:   10,
   501  					EndHour:       timestamppb.New(time.Unix(3600*10, 0)),
   502  					Counts: &bqpb.Segment_Counts{
   503  						TotalResults:          1,
   504  						TotalRuns:             1,
   505  						TotalVerdicts:         1,
   506  						ExpectedPassedResults: 1,
   507  					},
   508  				},
   509  			},
   510  		})
   511  		So(client.Insertions[1], ShouldResembleProto, &bqpb.TestVariantBranchRow{
   512  			Project:     "chromium",
   513  			TestId:      "test_2",
   514  			VariantHash: "hash_2",
   515  			RefHash:     "6de221242e011c91",
   516  			Variant:     "{\"k2\":\"v2\"}",
   517  			Ref: &pb.SourceRef{
   518  				System: &pb.SourceRef_Gitiles{
   519  					Gitiles: &pb.GitilesRef{
   520  						Host:    "host",
   521  						Project: "proj",
   522  						Ref:     "ref",
   523  					},
   524  				},
   525  			},
   526  			Segments: []*bqpb.Segment{
   527  				{
   528  					StartPosition: 10,
   529  					StartHour:     timestamppb.New(time.Unix(3600*10, 0)),
   530  					EndPosition:   10,
   531  					EndHour:       timestamppb.New(time.Unix(3600*10, 0)),
   532  					Counts: &bqpb.Segment_Counts{
   533  						TotalVerdicts:            1,
   534  						FlakyVerdicts:            1,
   535  						TotalRuns:                1,
   536  						FlakyRuns:                1,
   537  						TotalResults:             8,
   538  						UnexpectedResults:        4,
   539  						ExpectedPassedResults:    1,
   540  						ExpectedFailedResults:    1,
   541  						ExpectedCrashedResults:   1,
   542  						ExpectedAbortedResults:   1,
   543  						UnexpectedPassedResults:  1,
   544  						UnexpectedFailedResults:  1,
   545  						UnexpectedCrashedResults: 1,
   546  						UnexpectedAbortedResults: 1,
   547  					},
   548  				},
   549  			},
   550  		})
   551  
   552  		So(verdictCounter.Get(ctx, "chromium", "ingested"), ShouldEqual, 2)
   553  	})
   554  
   555  	Convey(`Analyze batch run analysis got change point`, t, func() {
   556  		ctx := newContext(t)
   557  		exporter, client := fakeExporter()
   558  
   559  		// Store some existing data in spanner first.
   560  		sourcesMap := tu.SampleSourcesMap(10)
   561  		positions := make([]int, 2000)
   562  		total := make([]int, 2000)
   563  		hasUnexpected := make([]int, 2000)
   564  		for i := 0; i < 2000; i++ {
   565  			positions[i] = i + 1
   566  			total[i] = 1
   567  			if i >= 100 {
   568  				hasUnexpected[i] = 1
   569  			}
   570  		}
   571  		vs := inputbuffer.Verdicts(positions, total, hasUnexpected)
   572  		ref := pbutil.SourceRefFromSources(sourcesMap["sources_id"])
   573  		tvb := &testvariantbranch.Entry{
   574  			IsNew:       true,
   575  			Project:     "chromium",
   576  			TestID:      "test_1",
   577  			VariantHash: "hash_1",
   578  			SourceRef:   ref,
   579  			RefHash:     pbutil.SourceRefHash(ref),
   580  			Variant: &pb.Variant{
   581  				Def: map[string]string{
   582  					"k": "v",
   583  				},
   584  			},
   585  			InputBuffer: &inputbuffer.Buffer{
   586  				HotBuffer: inputbuffer.History{
   587  					Verdicts: []inputbuffer.PositionVerdict{},
   588  				},
   589  				ColdBuffer: inputbuffer.History{
   590  					Verdicts: vs,
   591  				},
   592  				IsColdBufferDirty: true,
   593  			},
   594  		}
   595  		var hs inputbuffer.HistorySerializer
   596  		mutation, err := tvb.ToMutation(&hs)
   597  		So(err, ShouldBeNil)
   598  		testutil.MustApply(ctx, mutation)
   599  
   600  		// Insert a new verdict.
   601  		payload := tu.SamplePayload()
   602  		const ingestedVerdictHour = 55
   603  		payload.PartitionTime = timestamppb.New(time.Unix(ingestedVerdictHour*3600, 0))
   604  
   605  		tvs := []*rdbpb.TestVariant{
   606  			{
   607  				TestId:      "test_1",
   608  				VariantHash: "hash_1",
   609  				Status:      rdbpb.TestVariantStatus_EXPECTED,
   610  				Results: []*rdbpb.TestResultBundle{
   611  					{
   612  						Result: &rdbpb.TestResult{
   613  							Name:   "invocations/abc/tests/xyz",
   614  							Status: rdbpb.TestStatus_PASS,
   615  						},
   616  					},
   617  				},
   618  				SourcesId: "sources_id",
   619  			},
   620  		}
   621  
   622  		err = analyzeSingleBatch(ctx, tvs, payload, sourcesMap, exporter)
   623  		So(err, ShouldBeNil)
   624  		So(countCheckPoint(ctx), ShouldEqual, 1)
   625  
   626  		// Check invocations.
   627  		invs := fetchInvocations(ctx)
   628  		So(invs, ShouldResemble, []Invocation{
   629  			{
   630  				Project:              "chromium",
   631  				InvocationID:         "abc",
   632  				IngestedInvocationID: "build-1234",
   633  			},
   634  		})
   635  
   636  		// Check test variant branch.
   637  		tvbs, err := FetchTestVariantBranches(ctx)
   638  		So(err, ShouldBeNil)
   639  		So(len(tvbs), ShouldEqual, 1)
   640  		tvb = tvbs[0]
   641  
   642  		// Setup bucket expectations.
   643  		// Only statistics for verdicts evicted from the output buffer should be
   644  		// included in the statistics.
   645  		var expectedBuckets []*cpb.Statistics_HourBucket
   646  		for i := 1; i <= 100; i++ {
   647  			bucket := &cpb.Statistics_HourBucket{
   648  				Hour:          int64(i),
   649  				TotalVerdicts: 1,
   650  			}
   651  			expectedBuckets = append(expectedBuckets, bucket)
   652  			if bucket.Hour == ingestedVerdictHour {
   653  				// Add one for the verdict we just ingested.
   654  				bucket.TotalVerdicts += 1
   655  			}
   656  		}
   657  
   658  		So(tvb, ShouldResembleProto, &testvariantbranch.Entry{
   659  			Project:     "chromium",
   660  			TestID:      "test_1",
   661  			VariantHash: "hash_1",
   662  			RefHash:     pbutil.SourceRefHash(pbutil.SourceRefFromSources(sourcesMap["sources_id"])),
   663  			Variant: &pb.Variant{
   664  				Def: map[string]string{
   665  					"k": "v",
   666  				},
   667  			},
   668  			SourceRef: &pb.SourceRef{
   669  				System: &pb.SourceRef_Gitiles{
   670  					Gitiles: &pb.GitilesRef{
   671  						Host:    "host",
   672  						Project: "proj",
   673  						Ref:     "ref",
   674  					},
   675  				},
   676  			},
   677  			InputBuffer: &inputbuffer.Buffer{
   678  				HotBuffer: inputbuffer.History{
   679  					Verdicts: []inputbuffer.PositionVerdict{},
   680  				},
   681  				ColdBuffer: inputbuffer.History{
   682  					Verdicts: vs[100:],
   683  				},
   684  				HotBufferCapacity:  inputbuffer.DefaultHotBufferCapacity,
   685  				ColdBufferCapacity: inputbuffer.DefaultColdBufferCapacity,
   686  			},
   687  			FinalizingSegment: &cpb.Segment{
   688  				State:                        cpb.SegmentState_FINALIZING,
   689  				HasStartChangepoint:          true,
   690  				StartPosition:                101,
   691  				StartHour:                    timestamppb.New(time.Unix(101*3600, 0)),
   692  				FinalizedCounts:              &cpb.Counts{},
   693  				StartPositionLowerBound_99Th: 100,
   694  				StartPositionUpperBound_99Th: 101,
   695  			},
   696  			FinalizedSegments: &cpb.Segments{
   697  				Segments: []*cpb.Segment{
   698  					{
   699  						State:               cpb.SegmentState_FINALIZED,
   700  						HasStartChangepoint: false,
   701  						StartPosition:       1,
   702  						StartHour:           timestamppb.New(time.Unix(3600, 0)),
   703  						EndPosition:         100,
   704  						EndHour:             timestamppb.New(time.Unix(100*3600, 0)),
   705  						FinalizedCounts: &cpb.Counts{
   706  							TotalResults:          101,
   707  							TotalRuns:             101,
   708  							TotalVerdicts:         101,
   709  							ExpectedPassedResults: 101,
   710  						},
   711  					},
   712  				},
   713  			},
   714  			Statistics: &cpb.Statistics{
   715  				HourlyBuckets: expectedBuckets,
   716  			},
   717  		})
   718  		So(len(client.Insertions), ShouldEqual, 1)
   719  		So(verdictCounter.Get(ctx, "chromium", "ingested"), ShouldEqual, 1)
   720  	})
   721  
   722  	Convey(`Analyze batch should apply retention policy`, t, func() {
   723  		ctx := newContext(t)
   724  		exporter, client := fakeExporter()
   725  
   726  		// Store some existing data in spanner first.
   727  		sourcesMap := tu.SampleSourcesMap(10)
   728  
   729  		// Set up 110 finalized segments.
   730  		finalizedSegments := []*cpb.Segment{}
   731  		for i := 0; i < 110; i++ {
   732  			finalizedSegments = append(finalizedSegments, &cpb.Segment{
   733  				EndHour:         timestamppb.New(time.Unix(int64(i*3600), 0)),
   734  				FinalizedCounts: &cpb.Counts{},
   735  			})
   736  		}
   737  		sourceRef := pbutil.SourceRefFromSources(sourcesMap["sources_id"])
   738  		tvb := &testvariantbranch.Entry{
   739  			IsNew:       true,
   740  			Project:     "chromium",
   741  			TestID:      "test_1",
   742  			VariantHash: "hash_1",
   743  			SourceRef:   sourceRef,
   744  			RefHash:     pbutil.SourceRefHash(sourceRef),
   745  			Variant: &pb.Variant{
   746  				Def: map[string]string{
   747  					"k": "v",
   748  				},
   749  			},
   750  			InputBuffer: &inputbuffer.Buffer{
   751  				HotBuffer: inputbuffer.History{
   752  					Verdicts: []inputbuffer.PositionVerdict{
   753  						{
   754  							CommitPosition:       1,
   755  							IsSimpleExpectedPass: true,
   756  						},
   757  					},
   758  				},
   759  				ColdBuffer: inputbuffer.History{
   760  					Verdicts: []inputbuffer.PositionVerdict{},
   761  				},
   762  				IsColdBufferDirty: true,
   763  			},
   764  			FinalizedSegments: &cpb.Segments{Segments: finalizedSegments},
   765  		}
   766  		var hs inputbuffer.HistorySerializer
   767  		mutation, err := tvb.ToMutation(&hs)
   768  		So(err, ShouldBeNil)
   769  		testutil.MustApply(ctx, mutation)
   770  
   771  		// Insert a new verdict.
   772  		payload := tu.SamplePayload()
   773  		const ingestedVerdictHour = 5*365*24 + 13
   774  		payload.PartitionTime = timestamppb.New(time.Unix(ingestedVerdictHour*3600, 0))
   775  
   776  		tvs := []*rdbpb.TestVariant{
   777  			{
   778  				TestId:      "test_1",
   779  				VariantHash: "hash_1",
   780  				Status:      rdbpb.TestVariantStatus_EXPECTED,
   781  				Results: []*rdbpb.TestResultBundle{
   782  					{
   783  						Result: &rdbpb.TestResult{
   784  							Name:   "invocations/abc/tests/xyz",
   785  							Status: rdbpb.TestStatus_PASS,
   786  						},
   787  					},
   788  				},
   789  				SourcesId: "sources_id",
   790  			},
   791  		}
   792  
   793  		err = analyzeSingleBatch(ctx, tvs, payload, sourcesMap, exporter)
   794  		So(err, ShouldBeNil)
   795  		So(countCheckPoint(ctx), ShouldEqual, 1)
   796  
   797  		// Check invocations.
   798  		invs := fetchInvocations(ctx)
   799  		So(invs, ShouldResemble, []Invocation{
   800  			{
   801  				Project:              "chromium",
   802  				InvocationID:         "abc",
   803  				IngestedInvocationID: "build-1234",
   804  			},
   805  		})
   806  
   807  		// Check test variant branch.
   808  		tvbs, err := FetchTestVariantBranches(ctx)
   809  		So(err, ShouldBeNil)
   810  		So(len(tvbs), ShouldEqual, 1)
   811  		tvb = tvbs[0]
   812  
   813  		So(tvb, ShouldResembleProto, &testvariantbranch.Entry{
   814  			Project:     "chromium",
   815  			TestID:      "test_1",
   816  			VariantHash: "hash_1",
   817  			RefHash:     pbutil.SourceRefHash(sourceRef),
   818  			Variant: &pb.Variant{
   819  				Def: map[string]string{
   820  					"k": "v",
   821  				},
   822  			},
   823  			SourceRef: &pb.SourceRef{
   824  				System: &pb.SourceRef_Gitiles{
   825  					Gitiles: &pb.GitilesRef{
   826  						Host:    "host",
   827  						Project: "proj",
   828  						Ref:     "ref",
   829  					},
   830  				},
   831  			},
   832  			InputBuffer: &inputbuffer.Buffer{
   833  				HotBuffer: inputbuffer.History{
   834  					Verdicts: []inputbuffer.PositionVerdict{
   835  						{
   836  							CommitPosition:       1,
   837  							IsSimpleExpectedPass: true,
   838  						},
   839  						{
   840  							CommitPosition:       10,
   841  							IsSimpleExpectedPass: true,
   842  							Hour:                 payload.PartitionTime.AsTime(),
   843  						},
   844  					},
   845  				},
   846  				ColdBuffer: inputbuffer.History{
   847  					Verdicts: []inputbuffer.PositionVerdict{},
   848  				},
   849  				HotBufferCapacity:  inputbuffer.DefaultHotBufferCapacity,
   850  				ColdBufferCapacity: inputbuffer.DefaultColdBufferCapacity,
   851  			},
   852  
   853  			FinalizedSegments: &cpb.Segments{
   854  				Segments: finalizedSegments[14:],
   855  			},
   856  		})
   857  		So(len(client.Insertions), ShouldEqual, 1)
   858  		So(verdictCounter.Get(ctx, "chromium", "ingested"), ShouldEqual, 1)
   859  	})
   860  }
   861  
   862  func TestOutOfOrderVerdict(t *testing.T) {
   863  	Convey("Out of order verdict", t, func() {
   864  		sourcesMap := tu.SampleSourcesMap(10)
   865  		sources := sourcesMap["sources_id"]
   866  		Convey("No test variant branch", func() {
   867  			So(isOutOfOrderAndShouldBeDiscarded(nil, sources), ShouldBeFalse)
   868  		})
   869  
   870  		Convey("No finalizing or finalized segment", func() {
   871  			tvb := &testvariantbranch.Entry{}
   872  			So(isOutOfOrderAndShouldBeDiscarded(tvb, sources), ShouldBeFalse)
   873  		})
   874  
   875  		Convey("Have finalizing segments", func() {
   876  			tvb := finalizingTvbWithPositions([]int{1}, []int{})
   877  			So(isOutOfOrderAndShouldBeDiscarded(tvb, sources), ShouldBeFalse)
   878  			tvb = finalizingTvbWithPositions([]int{}, []int{1})
   879  			So(isOutOfOrderAndShouldBeDiscarded(tvb, sources), ShouldBeFalse)
   880  			tvb = finalizingTvbWithPositions([]int{8, 13}, []int{7, 9})
   881  			So(isOutOfOrderAndShouldBeDiscarded(tvb, sources), ShouldBeFalse)
   882  			tvb = finalizingTvbWithPositions([]int{11, 15}, []int{6, 8})
   883  			So(isOutOfOrderAndShouldBeDiscarded(tvb, sources), ShouldBeFalse)
   884  			tvb = finalizingTvbWithPositions([]int{11, 15}, []int{10, 16})
   885  			So(isOutOfOrderAndShouldBeDiscarded(tvb, sources), ShouldBeFalse)
   886  			tvb = finalizingTvbWithPositions([]int{11, 15}, []int{12, 16})
   887  			So(isOutOfOrderAndShouldBeDiscarded(tvb, sources), ShouldBeTrue)
   888  		})
   889  	})
   890  }
   891  
   892  func countCheckPoint(ctx context.Context) int {
   893  	st := spanner.NewStatement(`
   894  			SELECT *
   895  			FROM TestVariantBranchCheckPoint
   896  		`)
   897  	it := span.Query(span.Single(ctx), st)
   898  	count := 0
   899  	err := it.Do(func(r *spanner.Row) error {
   900  		count++
   901  		return nil
   902  	})
   903  	So(err, ShouldBeNil)
   904  	return count
   905  }
   906  
   907  func fetchInvocations(ctx context.Context) []Invocation {
   908  	st := spanner.NewStatement(`
   909  			SELECT Project, InvocationID, IngestedInvocationID
   910  			FROM Invocations
   911  			ORDER BY InvocationID
   912  		`)
   913  	it := span.Query(span.Single(ctx), st)
   914  	results := []Invocation{}
   915  	err := it.Do(func(r *spanner.Row) error {
   916  		var b spanutil.Buffer
   917  		inv := Invocation{}
   918  		err := b.FromSpanner(r, &inv.Project, &inv.InvocationID, &inv.IngestedInvocationID)
   919  		if err != nil {
   920  			return err
   921  		}
   922  		results = append(results, inv)
   923  		return nil
   924  	})
   925  	So(err, ShouldBeNil)
   926  	return results
   927  }
   928  
   929  func testVariants(n int) []*rdbpb.TestVariant {
   930  	tvs := make([]*rdbpb.TestVariant, n)
   931  	for i := 0; i < n; i++ {
   932  		tvs[i] = &rdbpb.TestVariant{
   933  			TestId:      fmt.Sprintf("test_%d", i),
   934  			VariantHash: fmt.Sprintf("hash_%d", i),
   935  			SourcesId:   "sources_id",
   936  		}
   937  	}
   938  	return tvs
   939  }
   940  
   941  func finalizingTvbWithPositions(hotPositions []int, coldPositions []int) *testvariantbranch.Entry {
   942  	tvb := &testvariantbranch.Entry{
   943  		FinalizingSegment: &cpb.Segment{},
   944  		InputBuffer:       &inputbuffer.Buffer{},
   945  	}
   946  	for _, pos := range hotPositions {
   947  		tvb.InputBuffer.HotBuffer.Verdicts = append(tvb.InputBuffer.HotBuffer.Verdicts, inputbuffer.PositionVerdict{
   948  			CommitPosition: pos,
   949  		})
   950  	}
   951  
   952  	for _, pos := range coldPositions {
   953  		tvb.InputBuffer.ColdBuffer.Verdicts = append(tvb.InputBuffer.ColdBuffer.Verdicts, inputbuffer.PositionVerdict{
   954  			CommitPosition: pos,
   955  		})
   956  	}
   957  	return tvb
   958  }
   959  
   960  func newContext(t *testing.T) context.Context {
   961  	ctx := memory.Use(testutil.IntegrationTestContext(t))
   962  	So(config.SetTestConfig(ctx, tu.TestConfig()), ShouldBeNil)
   963  	return ctx
   964  }
   965  
   966  func fakeExporter() (*bqexporter.Exporter, *bqexporter.FakeClient) {
   967  	client := bqexporter.NewFakeClient()
   968  	exporter := bqexporter.NewExporter(client)
   969  	return exporter, client
   970  }