go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/testresults/stability/test_data.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 stability
    16  
    17  import (
    18  	"context"
    19  	"time"
    20  
    21  	"go.chromium.org/luci/server/span"
    22  
    23  	"go.chromium.org/luci/analysis/internal/testresults"
    24  	"go.chromium.org/luci/analysis/pbutil"
    25  	pb "go.chromium.org/luci/analysis/proto/v1"
    26  )
    27  
    28  var referenceTime = time.Date(2022, time.June, 17, 8, 0, 0, 0, time.UTC)
    29  
    30  // CreateQueryStabilityTestData creates test data in Spanner for testing
    31  // QueryStability.
    32  func CreateQueryStabilityTestData(ctx context.Context) error {
    33  	var1 := pbutil.Variant("key1", "val1", "key2", "val1")
    34  	var2 := pbutil.Variant("key1", "val2", "key2", "val1")
    35  	var3 := pbutil.Variant("key1", "val2", "key2", "val2")
    36  
    37  	onBranch := pbutil.SourceRefHash(&pb.SourceRef{
    38  		System: &pb.SourceRef_Gitiles{
    39  			Gitiles: &pb.GitilesRef{
    40  				Host:    "mysources.googlesource.com",
    41  				Project: "myproject/src",
    42  				Ref:     "refs/heads/mybranch",
    43  			},
    44  		},
    45  	})
    46  	otherBranch := pbutil.SourceRefHash(&pb.SourceRef{
    47  		System: &pb.SourceRef_Gitiles{
    48  			Gitiles: &pb.GitilesRef{
    49  				Host:    "mysources.googlesource.com",
    50  				Project: "myproject/src",
    51  				Ref:     "refs/heads/otherbranch",
    52  			},
    53  		},
    54  	})
    55  
    56  	type verdict struct {
    57  		partitionTime time.Time
    58  		variant       *pb.Variant
    59  		invocationID  string
    60  		runStatuses   []testresults.RunStatus
    61  		sources       testresults.Sources
    62  	}
    63  
    64  	changelists := func(clOwnerKind pb.ChangelistOwnerKind, numbers ...int64) []testresults.Changelist {
    65  		var changelists []testresults.Changelist
    66  		for _, clNum := range numbers {
    67  			changelists = append(changelists, testresults.Changelist{
    68  				Host:      "mygerrit-review.googlesource.com",
    69  				Change:    clNum,
    70  				Patchset:  5,
    71  				OwnerKind: clOwnerKind,
    72  			})
    73  		}
    74  		return changelists
    75  	}
    76  
    77  	_, err := span.ReadWriteTransaction(ctx, func(ctx context.Context) error {
    78  		// pass, fail is shorthand here for expected and unexpected run,
    79  		// where for the purposes of this RPC, a flaky run counts as
    80  		// "expected" (as it has at least one expected result).
    81  		failPass := []testresults.RunStatus{testresults.Unexpected, testresults.Flaky}
    82  		failPassPass := []testresults.RunStatus{testresults.Unexpected, testresults.Flaky, testresults.Flaky}
    83  		pass := []testresults.RunStatus{testresults.Flaky}
    84  		fail := []testresults.RunStatus{testresults.Unexpected}
    85  		failFail := []testresults.RunStatus{testresults.Unexpected, testresults.Unexpected}
    86  
    87  		day := 24 * time.Hour
    88  
    89  		automationOwner := pb.ChangelistOwnerKind_AUTOMATION
    90  		humanOwner := pb.ChangelistOwnerKind_HUMAN
    91  		unspecifiedOwner := pb.ChangelistOwnerKind_CHANGELIST_OWNER_UNSPECIFIED
    92  
    93  		verdicts := []verdict{
    94  			{
    95  				partitionTime: referenceTime.Add(-14*day - time.Microsecond),
    96  				variant:       var1,
    97  				// Partition time too early, so should be ignored.
    98  				invocationID: "sourceverdict0-ignore-partitiontime",
    99  				runStatuses:  failPass,
   100  				sources: testresults.Sources{
   101  					RefHash:  onBranch,
   102  					Position: 1,
   103  				},
   104  			},
   105  			{
   106  				partitionTime: referenceTime.Add(-13*day - 1*time.Hour),
   107  				variant:       var1,
   108  				invocationID:  "sourceverdict1",
   109  				runStatuses:   failPass,
   110  				sources: testresults.Sources{
   111  					RefHash:  onBranch,
   112  					Position: 90,
   113  				},
   114  			},
   115  			{
   116  				partitionTime: referenceTime.Add(-12 * day),
   117  				variant:       var1,
   118  				invocationID:  "sourceverdict2",
   119  				runStatuses:   failPass,
   120  				sources: testresults.Sources{
   121  					RefHash:  onBranch,
   122  					Position: 100,
   123  				},
   124  			},
   125  			{
   126  				partitionTime: referenceTime.Add(-10 * day),
   127  				variant:       var1,
   128  				invocationID:  "sourceverdict3-part1",
   129  				runStatuses:   fail,
   130  				sources: testresults.Sources{
   131  					RefHash:     onBranch,
   132  					Position:    100,
   133  					Changelists: changelists(humanOwner, 1),
   134  				},
   135  			},
   136  			{
   137  				partitionTime: referenceTime.Add(-9 * day),
   138  				variant:       var1,
   139  				// Should combine with sourceverdict3-part1 to produce a run-flaky verdict, as it
   140  				// has the same sources under test.
   141  				invocationID: "sourceverdict3-part2",
   142  				runStatuses:  pass,
   143  				sources: testresults.Sources{
   144  					RefHash:     onBranch,
   145  					Position:    100,
   146  					Changelists: changelists(humanOwner, 1),
   147  				},
   148  			},
   149  			{
   150  				partitionTime: referenceTime.Add(-8*day - 3*time.Hour),
   151  				variant:       var1,
   152  				// source verdict 3 should be used preferentially as it is flaky
   153  				// and tests the same CL.
   154  				invocationID: "sourceverdict4-ignore",
   155  				runStatuses:  pass,
   156  				sources: testresults.Sources{
   157  					RefHash:     onBranch,
   158  					Position:    120,
   159  					Changelists: changelists(humanOwner, 1),
   160  				},
   161  			},
   162  			{
   163  				partitionTime: referenceTime.Add(-8*day - 2*time.Hour),
   164  				variant:       var1,
   165  				invocationID:  "sourceverdict5",
   166  				runStatuses:   pass,
   167  				sources: testresults.Sources{
   168  					RefHash:     onBranch,
   169  					Position:    120,
   170  					Changelists: changelists(unspecifiedOwner, 2),
   171  				},
   172  			},
   173  			{
   174  				partitionTime: referenceTime.Add(-8*day - time.Hour),
   175  				variant:       var1,
   176  				invocationID:  "sourceverdict6",
   177  				runStatuses:   failPass,
   178  				sources: testresults.Sources{
   179  					RefHash:  onBranch,
   180  					Position: 120,
   181  					IsDirty:  true,
   182  				},
   183  			},
   184  			{
   185  				partitionTime: referenceTime.Add(-8 * day),
   186  				variant:       var1,
   187  				// Should be distinct from source verdict 5 because both have
   188  				// IsDirty set, so we cannot confirm the soruces are identical.
   189  				invocationID: "sourceverdict7",
   190  				runStatuses:  failPassPass,
   191  				sources: testresults.Sources{
   192  					RefHash:  onBranch,
   193  					Position: 120,
   194  					IsDirty:  true,
   195  				},
   196  			},
   197  			{
   198  				partitionTime: referenceTime.Add(-8 * day),
   199  				variant:       var1,
   200  				// Automation-authored, so should be ignored.
   201  				invocationID: "sourceverdict8-ignore",
   202  				runStatuses:  failPass,
   203  				sources: testresults.Sources{
   204  					RefHash:     onBranch,
   205  					Position:    120,
   206  					Changelists: changelists(automationOwner, 4),
   207  				},
   208  			},
   209  			{
   210  				partitionTime: referenceTime.Add(-7 * day),
   211  				variant:       var1,
   212  				invocationID:  "sourceverdict9-part1",
   213  				runStatuses:   failFail,
   214  				sources: testresults.Sources{
   215  					RefHash:  onBranch,
   216  					Position: 128,
   217  				},
   218  			},
   219  			{
   220  				partitionTime: referenceTime.Add(-7 * day),
   221  				variant:       var1,
   222  				// Should merge with sourceverdict8-part1 due to sharing
   223  				// same sources.
   224  				invocationID: "sourceverdict9-part2",
   225  				runStatuses:  pass,
   226  				sources: testresults.Sources{
   227  					RefHash:  onBranch,
   228  					Position: 128,
   229  				},
   230  			},
   231  			{
   232  				partitionTime: referenceTime.Add(-7 * day),
   233  				variant:       var2,
   234  				// Different variant, so should be ignored.
   235  				invocationID: "sourceverdict10-ignore-var2",
   236  				runStatuses:  failPass,
   237  				sources: testresults.Sources{
   238  					RefHash:  onBranch,
   239  					Position: 130,
   240  				},
   241  			},
   242  			{
   243  				partitionTime: referenceTime.Add(-7 * day),
   244  				variant:       var3,
   245  				// For variant 3.
   246  				invocationID: "sourceverdict11",
   247  				runStatuses:  failPass,
   248  				sources: testresults.Sources{
   249  					RefHash:  otherBranch,
   250  					Position: 130,
   251  				},
   252  			},
   253  			{
   254  				partitionTime: referenceTime.Add(-7 * day),
   255  				variant:       var1,
   256  				// Other branch, so should be ignored.
   257  				invocationID: "sourceverdict12-ignore-offbranch",
   258  				runStatuses:  failPass,
   259  				sources: testresults.Sources{
   260  					RefHash:  otherBranch,
   261  					Position: 130,
   262  				},
   263  			},
   264  			{
   265  				partitionTime: referenceTime.Add(-7 * day),
   266  				variant:       var1,
   267  				invocationID:  "sourceverdict13-ignore-no-sources",
   268  				runStatuses:   pass,
   269  				sources: testresults.Sources{
   270  					// Has no branch and position information, so should be ignored.
   271  					Changelists: changelists(pb.ChangelistOwnerKind_HUMAN, 10),
   272  				},
   273  			},
   274  			{
   275  				partitionTime: referenceTime.Add(-7 * day),
   276  				variant:       var1,
   277  				// A result for the same CL as being queried, so should be ignored
   278  				// to avoid CLs contributing to their own exoneration.
   279  				invocationID: "sourceverdict14-ignore-same-cl-as-query",
   280  				runStatuses:  failPass,
   281  				sources: testresults.Sources{
   282  					RefHash:     otherBranch,
   283  					Position:    130,
   284  					Changelists: changelists(pb.ChangelistOwnerKind_HUMAN, 888888),
   285  				},
   286  			},
   287  			{
   288  				partitionTime: referenceTime.Add(-6 * day),
   289  				variant:       var1,
   290  				invocationID:  "sourceverdict15",
   291  				runStatuses:   pass,
   292  				sources: testresults.Sources{
   293  					RefHash:  onBranch,
   294  					Position: 129,
   295  				},
   296  			},
   297  			// Begin consecutive failure streak.
   298  			{
   299  				partitionTime: referenceTime.Add(-6 * day),
   300  				variant:       var1,
   301  				invocationID:  "sourceverdict16-part1",
   302  				runStatuses:   failFail,
   303  				sources: testresults.Sources{
   304  					RefHash:  onBranch,
   305  					Position: 130,
   306  				},
   307  			},
   308  			{
   309  				partitionTime: referenceTime.Add(-6 * day),
   310  				variant:       var1,
   311  				// Should merge with sourceverdict13-part1 due to sharing
   312  				// same sources.
   313  				invocationID: "sourceverdict16-part2",
   314  				runStatuses:  failFail,
   315  				sources: testresults.Sources{
   316  					RefHash:  onBranch,
   317  					Position: 130,
   318  				},
   319  			},
   320  			{
   321  				partitionTime: referenceTime.Add(-1 * day),
   322  				variant:       var1,
   323  				invocationID:  "sourceverdict17",
   324  				// Only one run should be used as the verdict relates to presubmit testing.
   325  				runStatuses: failPass,
   326  				sources: testresults.Sources{
   327  					RefHash:     onBranch,
   328  					Changelists: changelists(pb.ChangelistOwnerKind_HUMAN, 888777),
   329  					Position:    140,
   330  				},
   331  			},
   332  		}
   333  
   334  		for _, v := range verdicts {
   335  			baseTestResult := testresults.NewTestResult().
   336  				WithProject("project").
   337  				WithTestID("test_id").
   338  				WithVariantHash(pbutil.VariantHash(v.variant)).
   339  				WithPartitionTime(v.partitionTime).
   340  				WithIngestedInvocationID(v.invocationID).
   341  				WithSubRealm("realm").
   342  				WithStatus(pb.TestResultStatus_PASS).
   343  				WithSources(v.sources)
   344  
   345  			trs := testresults.NewTestVerdict().
   346  				WithBaseTestResult(baseTestResult.Build()).
   347  				WithRunStatus(v.runStatuses...).
   348  				Build()
   349  			for _, tr := range trs {
   350  				span.BufferWrite(ctx, tr.SaveUnverified())
   351  			}
   352  		}
   353  
   354  		return nil
   355  	})
   356  	return err
   357  }
   358  
   359  func QueryStabilitySampleRequest() QueryStabilityOptions {
   360  	var1 := pbutil.Variant("key1", "val1", "key2", "val1")
   361  	var3 := pbutil.Variant("key1", "val2", "key2", "val2")
   362  	testVariants := []*pb.QueryTestVariantStabilityRequest_TestVariantPosition{
   363  		{
   364  			TestId:  "test_id",
   365  			Variant: var1,
   366  			Sources: &pb.Sources{
   367  				GitilesCommit: &pb.GitilesCommit{
   368  					Host:       "mysources.googlesource.com",
   369  					Project:    "myproject/src",
   370  					Ref:        "refs/heads/mybranch",
   371  					CommitHash: "aabbccddeeff00112233aabbccddeeff00112233",
   372  					Position:   130,
   373  				},
   374  				Changelists: []*pb.GerritChange{
   375  					{
   376  						Host:     "mygerrit-review.googlesource.com",
   377  						Project:  "mygerrit/src",
   378  						Change:   888888,
   379  						Patchset: 1,
   380  					},
   381  				},
   382  			},
   383  		},
   384  		{
   385  			TestId:  "test_id",
   386  			Variant: var3,
   387  			Sources: &pb.Sources{
   388  				GitilesCommit: &pb.GitilesCommit{
   389  					Host:       "mysources.googlesource.com",
   390  					Project:    "myproject/src",
   391  					Ref:        "refs/heads/otherbranch",
   392  					CommitHash: "00bbccddeeff00112233aabbccddeeff00112233",
   393  					Position:   120,
   394  				},
   395  			},
   396  		},
   397  	}
   398  	return QueryStabilityOptions{
   399  		Project:              "project",
   400  		SubRealms:            []string{"realm"},
   401  		TestVariantPositions: testVariants,
   402  		Criteria:             CreateSampleStabilityCriteria(),
   403  		AsAtTime:             time.Date(2022, time.June, 17, 8, 0, 0, 0, time.UTC),
   404  	}
   405  }
   406  
   407  func CreateSampleStabilityCriteria() *pb.TestStabilityCriteria {
   408  	return &pb.TestStabilityCriteria{
   409  		FailureRate: &pb.TestStabilityCriteria_FailureRateCriteria{
   410  			FailureThreshold:            7,
   411  			ConsecutiveFailureThreshold: 3,
   412  		},
   413  		FlakeRate: &pb.TestStabilityCriteria_FlakeRateCriteria{
   414  			// Use 5 instead of a more typical value like 100 because of the
   415  			// limited sample data.
   416  			MinWindow:          5,
   417  			FlakeThreshold:     2,
   418  			FlakeRateThreshold: 0.01,
   419  		},
   420  	}
   421  }
   422  
   423  // QueryStabilitySampleResponse returns expected response data from QueryFailureRate
   424  // after being invoked with QueryFailureRateSampleRequest.
   425  // It is assumed test data was setup with CreateQueryFailureRateTestData.
   426  func QueryStabilitySampleResponse() []*pb.TestVariantStabilityAnalysis {
   427  	var1 := pbutil.Variant("key1", "val1", "key2", "val1")
   428  	var3 := pbutil.Variant("key1", "val2", "key2", "val2")
   429  
   430  	analysis := []*pb.TestVariantStabilityAnalysis{
   431  		{
   432  			TestId:  "test_id",
   433  			Variant: var1,
   434  			FailureRate: &pb.TestVariantStabilityAnalysis_FailureRate{
   435  				IsMet:                         true,
   436  				UnexpectedTestRuns:            8,
   437  				ConsecutiveUnexpectedTestRuns: 5,
   438  				RecentVerdicts: []*pb.TestVariantStabilityAnalysis_FailureRate_RecentVerdict{
   439  					{
   440  						Position: 140,
   441  						Changelists: []*pb.Changelist{
   442  							{
   443  								Host:      "mygerrit-review.googlesource.com",
   444  								Change:    888777,
   445  								Patchset:  5,
   446  								OwnerKind: pb.ChangelistOwnerKind_HUMAN,
   447  							},
   448  						},
   449  						Invocations: []string{"sourceverdict17"},
   450  						// Unexpected + expected run flattened down to only an unexpected run due to
   451  						// presubmit results only being allowed to contribute 1 test run.
   452  						UnexpectedRuns: 1,
   453  						TotalRuns:      1,
   454  					},
   455  					{
   456  						Position:       130, // Query position.
   457  						Invocations:    []string{"sourceverdict16-part1", "sourceverdict16-part2"},
   458  						UnexpectedRuns: 4,
   459  						TotalRuns:      4,
   460  					},
   461  					{
   462  						Position:    129,
   463  						Invocations: []string{"sourceverdict15"},
   464  						TotalRuns:   1,
   465  					},
   466  					{
   467  						Position:       128,
   468  						Invocations:    []string{"sourceverdict9-part1", "sourceverdict9-part2"},
   469  						UnexpectedRuns: 2,
   470  						TotalRuns:      3,
   471  					},
   472  					{
   473  						Position:       120,
   474  						Invocations:    []string{"sourceverdict7"},
   475  						UnexpectedRuns: 1,
   476  						TotalRuns:      2, // Verdict truncated to 2 runs to keep total runs on or before query position <= 10.
   477  					},
   478  				},
   479  			},
   480  			FlakeRate: &pb.TestVariantStabilityAnalysis_FlakeRate{
   481  				IsMet:            true,
   482  				TotalVerdicts:    9,
   483  				RunFlakyVerdicts: 6,
   484  				FlakeExamples: []*pb.TestVariantStabilityAnalysis_FlakeRate_VerdictExample{
   485  					{
   486  						Position: 140,
   487  						Changelists: []*pb.Changelist{
   488  							{
   489  								Host:      "mygerrit-review.googlesource.com",
   490  								Change:    888777,
   491  								Patchset:  5,
   492  								OwnerKind: pb.ChangelistOwnerKind_HUMAN,
   493  							},
   494  						},
   495  						Invocations: []string{"sourceverdict17"},
   496  					},
   497  					{
   498  						Position:    128,
   499  						Invocations: []string{"sourceverdict9-part1", "sourceverdict9-part2"},
   500  					},
   501  					{
   502  						Position:    120,
   503  						Invocations: []string{"sourceverdict7"},
   504  					},
   505  					{
   506  						Position:    120,
   507  						Invocations: []string{"sourceverdict6"},
   508  					},
   509  					{
   510  						Position:    100,
   511  						Invocations: []string{"sourceverdict3-part1", "sourceverdict3-part2"},
   512  						Changelists: []*pb.Changelist{
   513  							{
   514  								Host:      "mygerrit-review.googlesource.com",
   515  								Change:    1,
   516  								Patchset:  5,
   517  								OwnerKind: pb.ChangelistOwnerKind_HUMAN,
   518  							},
   519  						},
   520  					},
   521  					{
   522  						Position:    100,
   523  						Invocations: []string{"sourceverdict2"},
   524  					},
   525  				},
   526  				StartPosition: 100,
   527  				EndPosition:   140,
   528  			},
   529  		},
   530  		{
   531  			TestId:  "test_id",
   532  			Variant: var3,
   533  			FailureRate: &pb.TestVariantStabilityAnalysis_FailureRate{
   534  				IsMet:                         false,
   535  				UnexpectedTestRuns:            1,
   536  				ConsecutiveUnexpectedTestRuns: 1,
   537  				RecentVerdicts: []*pb.TestVariantStabilityAnalysis_FailureRate_RecentVerdict{
   538  					{
   539  						Position:       130,
   540  						Invocations:    []string{"sourceverdict11"},
   541  						UnexpectedRuns: 1,
   542  						TotalRuns:      2,
   543  					},
   544  				},
   545  			},
   546  			FlakeRate: &pb.TestVariantStabilityAnalysis_FlakeRate{
   547  				IsMet:            false,
   548  				RunFlakyVerdicts: 1,
   549  				TotalVerdicts:    1,
   550  				FlakeExamples: []*pb.TestVariantStabilityAnalysis_FlakeRate_VerdictExample{
   551  					{
   552  						Position:    130,
   553  						Invocations: []string{"sourceverdict11"},
   554  					},
   555  				},
   556  				StartPosition: 130,
   557  				EndPosition:   130,
   558  			},
   559  		},
   560  	}
   561  	return analysis
   562  }
   563  
   564  // QueryStabilitySampleResponseLargeWindow is the expected response from
   565  // QueryStability for FlakeRate.MinWindow = 100.
   566  func QueryStabilitySampleResponseLargeWindow() []*pb.TestVariantStabilityAnalysis {
   567  	rsp := QueryStabilitySampleResponse()
   568  	fr := rsp[0].FlakeRate
   569  	fr.TotalVerdicts++
   570  	fr.RunFlakyVerdicts++
   571  
   572  	// Include verdicts outside the normal window, but within the last 14 days.
   573  	fr.FlakeExamples = append(fr.FlakeExamples, &pb.TestVariantStabilityAnalysis_FlakeRate_VerdictExample{
   574  		Position:    90,
   575  		Invocations: []string{"sourceverdict1"},
   576  	})
   577  	fr.StartPosition = 90
   578  
   579  	return rsp
   580  }