go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/testresults/test_utils.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 testresults
    16  
    17  import (
    18  	"time"
    19  
    20  	pb "go.chromium.org/luci/analysis/proto/v1"
    21  )
    22  
    23  // TestResultBuilder provides methods to build a test result for testing.
    24  type TestResultBuilder struct {
    25  	result TestResult
    26  }
    27  
    28  func NewTestResult() TestResultBuilder {
    29  	d := time.Hour
    30  	result := TestResult{
    31  		Project:              "proj",
    32  		TestID:               "test_id",
    33  		PartitionTime:        time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC),
    34  		VariantHash:          "hash",
    35  		IngestedInvocationID: "inv-id",
    36  		RunIndex:             2,
    37  		ResultIndex:          3,
    38  		IsUnexpected:         true,
    39  		RunDuration:          &d,
    40  		Status:               pb.TestResultStatus_PASS,
    41  		ExonerationReasons:   nil,
    42  		SubRealm:             "realm",
    43  		Sources: Sources{
    44  			RefHash:  []byte{0, 1, 2, 3, 4, 5, 6, 7},
    45  			Position: 999444,
    46  			Changelists: []Changelist{
    47  				{
    48  					Host:     "mygerrit-review.googlesource.com",
    49  					Change:   12345678,
    50  					Patchset: 9,
    51  				},
    52  				{
    53  					Host:     "anothergerrit.gerrit.instance",
    54  					Change:   234568790,
    55  					Patchset: 1,
    56  				},
    57  			},
    58  			IsDirty: true,
    59  		},
    60  	}
    61  	return TestResultBuilder{
    62  		result: result,
    63  	}
    64  }
    65  
    66  func (b TestResultBuilder) WithProject(project string) TestResultBuilder {
    67  	b.result.Project = project
    68  	return b
    69  }
    70  
    71  func (b TestResultBuilder) WithTestID(testID string) TestResultBuilder {
    72  	b.result.TestID = testID
    73  	return b
    74  }
    75  
    76  func (b TestResultBuilder) WithPartitionTime(partitionTime time.Time) TestResultBuilder {
    77  	b.result.PartitionTime = partitionTime
    78  	return b
    79  }
    80  
    81  func (b TestResultBuilder) WithVariantHash(variantHash string) TestResultBuilder {
    82  	b.result.VariantHash = variantHash
    83  	return b
    84  }
    85  
    86  func (b TestResultBuilder) WithIngestedInvocationID(invID string) TestResultBuilder {
    87  	b.result.IngestedInvocationID = invID
    88  	return b
    89  }
    90  
    91  func (b TestResultBuilder) WithRunIndex(runIndex int64) TestResultBuilder {
    92  	b.result.RunIndex = runIndex
    93  	return b
    94  }
    95  
    96  func (b TestResultBuilder) WithResultIndex(resultIndex int64) TestResultBuilder {
    97  	b.result.ResultIndex = resultIndex
    98  	return b
    99  }
   100  
   101  func (b TestResultBuilder) WithIsUnexpected(unexpected bool) TestResultBuilder {
   102  	b.result.IsUnexpected = unexpected
   103  	return b
   104  }
   105  
   106  func (b TestResultBuilder) WithRunDuration(duration time.Duration) TestResultBuilder {
   107  	b.result.RunDuration = &duration
   108  	return b
   109  }
   110  
   111  func (b TestResultBuilder) WithoutRunDuration() TestResultBuilder {
   112  	b.result.RunDuration = nil
   113  	return b
   114  }
   115  
   116  func (b TestResultBuilder) WithStatus(status pb.TestResultStatus) TestResultBuilder {
   117  	b.result.Status = status
   118  	return b
   119  }
   120  
   121  func (b TestResultBuilder) WithExonerationReasons(exonerationReasons ...pb.ExonerationReason) TestResultBuilder {
   122  	b.result.ExonerationReasons = exonerationReasons
   123  	return b
   124  }
   125  
   126  func (b TestResultBuilder) WithoutExoneration() TestResultBuilder {
   127  	b.result.ExonerationReasons = nil
   128  	return b
   129  }
   130  
   131  func (b TestResultBuilder) WithSubRealm(subRealm string) TestResultBuilder {
   132  	b.result.SubRealm = subRealm
   133  	return b
   134  }
   135  
   136  func (b TestResultBuilder) WithSources(sources Sources) TestResultBuilder {
   137  	// Copy sources to avoid aliasing artifacts. Changes made to
   138  	// slices within the sources struct after this call should
   139  	// not propagate to the test result.
   140  	b.result.Sources = copySources(sources)
   141  	return b
   142  }
   143  
   144  func (b TestResultBuilder) WithIsFromBisection(value bool) TestResultBuilder {
   145  	b.result.IsFromBisection = value
   146  	return b
   147  }
   148  
   149  func (b TestResultBuilder) Build() *TestResult {
   150  	// Copy the result, so that calling further methods on the builder does
   151  	// not change the returned test verdict.
   152  	result := new(TestResult)
   153  	*result = b.result
   154  	result.Sources = copySources(b.result.Sources)
   155  	return result
   156  }
   157  
   158  // copySources makes a deep copy of the given code sources.
   159  func copySources(sources Sources) Sources {
   160  	var refHash []byte
   161  	if sources.RefHash != nil {
   162  		refHash = make([]byte, len(sources.RefHash))
   163  		copy(refHash, sources.RefHash)
   164  	}
   165  
   166  	cls := make([]Changelist, len(sources.Changelists))
   167  	copy(cls, sources.Changelists)
   168  
   169  	return Sources{
   170  		RefHash:     refHash,
   171  		Position:    sources.Position,
   172  		Changelists: cls,
   173  		IsDirty:     sources.IsDirty,
   174  	}
   175  }
   176  
   177  // TestVerdictBuilder provides methods to build a test variant for testing.
   178  type TestVerdictBuilder struct {
   179  	baseResult        TestResult
   180  	status            *pb.TestVerdictStatus
   181  	runStatuses       []RunStatus
   182  	passedAvgDuration *time.Duration
   183  }
   184  
   185  type RunStatus int64
   186  
   187  const (
   188  	Unexpected RunStatus = iota
   189  	Flaky
   190  	Expected
   191  )
   192  
   193  func NewTestVerdict() *TestVerdictBuilder {
   194  	result := new(TestVerdictBuilder)
   195  	result.baseResult = *NewTestResult().WithStatus(pb.TestResultStatus_PASS).Build()
   196  	status := pb.TestVerdictStatus_FLAKY
   197  	result.status = &status
   198  	result.runStatuses = nil
   199  	d := 919191 * time.Microsecond
   200  	result.passedAvgDuration = &d
   201  	return result
   202  }
   203  
   204  // WithBaseTestResult specifies a test result to use as the template for
   205  // the test variant's test results.
   206  func (b *TestVerdictBuilder) WithBaseTestResult(testResult *TestResult) *TestVerdictBuilder {
   207  	b.baseResult = *testResult
   208  	return b
   209  }
   210  
   211  // WithPassedAvgDuration specifies the average duration to use for
   212  // passed test results. If setting to a non-nil value, make sure
   213  // to set the result status as passed on the base test result if
   214  // using this option.
   215  func (b *TestVerdictBuilder) WithPassedAvgDuration(duration *time.Duration) *TestVerdictBuilder {
   216  	b.passedAvgDuration = duration
   217  	return b
   218  }
   219  
   220  // WithStatus specifies the status of the test verdict.
   221  func (b *TestVerdictBuilder) WithStatus(status pb.TestVerdictStatus) *TestVerdictBuilder {
   222  	b.status = &status
   223  	return b
   224  }
   225  
   226  // WithRunStatus specifies the status of runs of the test verdict.
   227  func (b *TestVerdictBuilder) WithRunStatus(runStatuses ...RunStatus) *TestVerdictBuilder {
   228  	b.runStatuses = runStatuses
   229  	return b
   230  }
   231  
   232  func applyStatus(trs []*TestResult, status pb.TestVerdictStatus) {
   233  	// Set all test results to unexpected, not exonerated by default.
   234  	for _, tr := range trs {
   235  		tr.IsUnexpected = true
   236  		tr.ExonerationReasons = nil
   237  	}
   238  	switch status {
   239  	case pb.TestVerdictStatus_EXONERATED:
   240  		for _, tr := range trs {
   241  			tr.ExonerationReasons = []pb.ExonerationReason{pb.ExonerationReason_OCCURS_ON_MAINLINE}
   242  		}
   243  	case pb.TestVerdictStatus_UNEXPECTED:
   244  		// No changes required.
   245  	case pb.TestVerdictStatus_EXPECTED:
   246  		allSkipped := true
   247  		for _, tr := range trs {
   248  			tr.IsUnexpected = false
   249  			if tr.Status != pb.TestResultStatus_SKIP {
   250  				allSkipped = false
   251  			}
   252  		}
   253  		// Make sure not all test results are SKIPPED, to avoid the status
   254  		// UNEXPECTEDLY_SKIPPED.
   255  		if allSkipped {
   256  			trs[0].Status = pb.TestResultStatus_CRASH
   257  		}
   258  	case pb.TestVerdictStatus_UNEXPECTEDLY_SKIPPED:
   259  		for _, tr := range trs {
   260  			tr.Status = pb.TestResultStatus_SKIP
   261  		}
   262  	case pb.TestVerdictStatus_FLAKY:
   263  		trs[0].IsUnexpected = false
   264  	default:
   265  		panic("status must be specified")
   266  	}
   267  }
   268  
   269  // applyRunStatus applies the given run status to the given test results.
   270  func applyRunStatus(trs []*TestResult, runStatus RunStatus) {
   271  	for _, tr := range trs {
   272  		tr.IsUnexpected = true
   273  	}
   274  	switch runStatus {
   275  	case Expected:
   276  		for _, tr := range trs {
   277  			tr.IsUnexpected = false
   278  		}
   279  	case Flaky:
   280  		trs[0].IsUnexpected = false
   281  	case Unexpected:
   282  		// All test results already unexpected.
   283  	}
   284  }
   285  
   286  func applyAvgPassedDuration(trs []*TestResult, passedAvgDuration *time.Duration) {
   287  	if passedAvgDuration == nil {
   288  		for _, tr := range trs {
   289  			if tr.Status == pb.TestResultStatus_PASS {
   290  				tr.RunDuration = nil
   291  			}
   292  		}
   293  		return
   294  	}
   295  
   296  	passCount := 0
   297  	for _, tr := range trs {
   298  		if tr.Status == pb.TestResultStatus_PASS {
   299  			passCount++
   300  		}
   301  	}
   302  	passIndex := 0
   303  	for _, tr := range trs {
   304  		if tr.Status == pb.TestResultStatus_PASS {
   305  			d := *passedAvgDuration
   306  			if passCount == 1 {
   307  				// If there is only one pass, assign it the
   308  				// set duration.
   309  				tr.RunDuration = &d
   310  				break
   311  			}
   312  			if passIndex == 0 && passCount%2 == 1 {
   313  				// If there are an odd number of passes, and
   314  				// more than one pass, assign the first pass
   315  				// a nil duration.
   316  				tr.RunDuration = nil
   317  			} else {
   318  				// Assigning alternating passes 2*d the duration
   319  				// and 0 duration, to keep the average correct.
   320  				if passIndex%2 == 0 {
   321  					d = d * 2
   322  					tr.RunDuration = &d
   323  				} else {
   324  					d = 0
   325  					tr.RunDuration = &d
   326  				}
   327  			}
   328  			passIndex++
   329  		}
   330  	}
   331  }
   332  
   333  func (b *TestVerdictBuilder) Build() []*TestResult {
   334  	runs := 2
   335  	if len(b.runStatuses) > 0 {
   336  		runs = len(b.runStatuses)
   337  	}
   338  
   339  	// Create two test results per run, to allow
   340  	// for all expected, all unexpected and
   341  	// flaky (mixed expected+unexpected) statuses
   342  	// to be represented.
   343  	trs := make([]*TestResult, 0, runs*2)
   344  	for i := 0; i < runs*2; i++ {
   345  		tr := new(TestResult)
   346  		*tr = b.baseResult
   347  		tr.RunIndex = int64(i / 2)
   348  		tr.ResultIndex = int64(i % 2)
   349  		trs = append(trs, tr)
   350  	}
   351  
   352  	// Normally only one of these should be set.
   353  	// If both are set, run statuses has precedence.
   354  	if b.status != nil {
   355  		applyStatus(trs, *b.status)
   356  	}
   357  	for i, runStatus := range b.runStatuses {
   358  		runTRs := trs[i*2 : (i+1)*2]
   359  		applyRunStatus(runTRs, runStatus)
   360  	}
   361  
   362  	applyAvgPassedDuration(trs, b.passedAvgDuration)
   363  	return trs
   364  }