go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/cmd/autogardener/blamelist_test.go (about)

     1  // Copyright 2022 The Fuchsia Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style license that can be
     3  // found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"testing"
     9  
    10  	"github.com/google/go-cmp/cmp"
    11  )
    12  
    13  func TestCalculateBlamelistDistances(t *testing.T) {
    14  	const pass = "PASS"
    15  	const failure = "FAIL"
    16  
    17  	// Mapping from builder name to the status of the test in question at each
    18  	// commit position.
    19  	allBuilds := map[string]map[int]string{
    20  		"bar": {
    21  			499: pass,
    22  			502: pass,
    23  			503: pass,
    24  			504: failure,
    25  			506: failure,
    26  			511: failure,
    27  		},
    28  		"foo": {
    29  			500: pass,
    30  			505: failure,
    31  			510: pass,
    32  			515: failure,
    33  		},
    34  		"allpass": {
    35  			499: pass,
    36  			502: pass,
    37  			511: pass,
    38  		},
    39  	}
    40  	// Mapping from commit (by position) to expected CI builder distances for
    41  	// that commit.
    42  	expected := map[int]map[string]int{
    43  		502: {
    44  			"bar": 2,
    45  			"foo": 0,
    46  		},
    47  		503: {
    48  			"bar": 1,
    49  			"foo": 0,
    50  		},
    51  		504: {
    52  			"bar": 0,
    53  			"foo": 0,
    54  		},
    55  		505: {
    56  			"bar": -1,
    57  			"foo": 0,
    58  		},
    59  	}
    60  
    61  	var results []nearbyTestResult
    62  	for builder, builds := range allBuilds {
    63  		for position, status := range builds {
    64  			results = append(results, nearbyTestResult{
    65  				Builder:        builder,
    66  				CommitPosition: position,
    67  				Failed:         status == failure,
    68  			})
    69  		}
    70  	}
    71  
    72  	var suspects []suspectCommit
    73  	for pos := range expected {
    74  		suspects = append(suspects, suspectCommit{
    75  			CommitPosition:     pos,
    76  			BlamelistDistances: make(map[string]int),
    77  		})
    78  	}
    79  
    80  	calculateBlamelistDistances(results, suspects)
    81  
    82  	got := make(map[int]map[string]int)
    83  	for _, suspect := range suspects {
    84  		got[suspect.CommitPosition] = suspect.BlamelistDistances
    85  	}
    86  
    87  	if diff := cmp.Diff(expected, got); diff != "" {
    88  		t.Errorf("Diff:\n%s", diff)
    89  	}
    90  }
    91  
    92  func TestScoreBlamelistDistances(t *testing.T) {
    93  	tests := []struct {
    94  		name      string
    95  		distances []int
    96  		// min/max are the inclusive range of allowed scores for the given CI
    97  		// blamelist distances. We don't require equality because the exact
    98  		// numbers don't matter that much; we just care that the scores follow a
    99  		// certain descending pattern as changes get further and further away
   100  		// from the first CI failure.
   101  		min int
   102  		max int
   103  	}{
   104  		{
   105  			// If a change is in the first-failure blamelist of many builders,
   106  			// it should have a very high score.
   107  			name:      "in the blamelist for many builders",
   108  			distances: []int{0, 0, 0, 0, 0},
   109  			min:       100,
   110  			max:       100,
   111  		},
   112  		{
   113  			// Fewer blamelists should lower the score slightly since there are
   114  			// fewer data points.
   115  			name:      "in the blamelist for two builders",
   116  			distances: []int{0, 0},
   117  			min:       90,
   118  			max:       99,
   119  		},
   120  		{
   121  			// If there's only one data point then the score should be a bit
   122  			// lower to reflect lack of confidence.
   123  			name:      "in the blamelist for one builder",
   124  			distances: []int{0},
   125  			min:       80,
   126  			max:       90,
   127  		},
   128  		{
   129  			// The culprit may have landed *before* the first failure for a few
   130  			// builders if the failure mode is flaky. The confidence level
   131  			// should be lower but still reasonably high.
   132  			name:      "in earlier blamelists",
   133  			distances: []int{0, 1, 2},
   134  			min:       60,
   135  			max:       70,
   136  		},
   137  		{
   138  			name:      "pretty far away from first failures",
   139  			distances: []int{6, 10, 5},
   140  			min:       15,
   141  			max:       25,
   142  		},
   143  		{
   144  			// A negative distance indicates that the change landed *after* the
   145  			// first failed build, which makes the change very unlikely to be
   146  			// the culprit unless it was a pre-existing failure mode that was
   147  			// made worse by the change.
   148  			name:      "change landed after first failure",
   149  			distances: []int{0, -1},
   150  			min:       0,
   151  			max:       30,
   152  		},
   153  		{
   154  			name:      "no blamelist data",
   155  			distances: nil,
   156  			min:       0,
   157  			max:       0,
   158  		},
   159  	}
   160  
   161  	for _, test := range tests {
   162  		t.Run(test.name, func(t *testing.T) {
   163  			score := scoreBlamelistDistances(test.distances)
   164  			if score < test.min || score > test.max {
   165  				t.Errorf("scoreBlamelistDistances(%d) = %d is outside range [%d, %d]",
   166  					test.distances, score, test.min, test.max,
   167  				)
   168  			} else {
   169  				t.Logf("scoreBlamelistDistances(%d) = %d", test.distances, score)
   170  			}
   171  		})
   172  	}
   173  }