go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/bugs/updater/clustering_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 updater
    16  
    17  import (
    18  	"context"
    19  	"encoding/hex"
    20  	"fmt"
    21  	"strings"
    22  
    23  	"cloud.google.com/go/bigquery"
    24  
    25  	"go.chromium.org/luci/analysis/internal/analysis"
    26  	"go.chromium.org/luci/analysis/internal/analysis/metrics"
    27  	"go.chromium.org/luci/analysis/internal/clustering"
    28  	"go.chromium.org/luci/analysis/internal/clustering/algorithms"
    29  	"go.chromium.org/luci/analysis/internal/clustering/algorithms/failurereason"
    30  	"go.chromium.org/luci/analysis/internal/clustering/algorithms/rulesalgorithm"
    31  	"go.chromium.org/luci/analysis/internal/clustering/algorithms/testname"
    32  	"go.chromium.org/luci/analysis/internal/config/compiledcfg"
    33  	configpb "go.chromium.org/luci/analysis/proto/config"
    34  	pb "go.chromium.org/luci/analysis/proto/v1"
    35  
    36  	. "github.com/smartystreets/goconvey/convey"
    37  )
    38  
    39  func emptyMetricValues() map[metrics.ID]metrics.TimewiseCounts {
    40  	result := make(map[metrics.ID]metrics.TimewiseCounts)
    41  	for _, m := range metrics.ComputedMetrics {
    42  		result[m.ID] = metrics.TimewiseCounts{}
    43  	}
    44  	return result
    45  }
    46  
    47  func makeTestNameCluster(config *compiledcfg.ProjectConfig, uniqifier int) *analysis.Cluster {
    48  	testID := fmt.Sprintf("testname-%v", uniqifier)
    49  	return &analysis.Cluster{
    50  		ClusterID: testIDClusterID(config, testID),
    51  		MetricValues: map[metrics.ID]metrics.TimewiseCounts{
    52  			metrics.Failures.ID: {
    53  				OneDay:   metrics.Counts{Residual: 9},
    54  				ThreeDay: metrics.Counts{Residual: 29},
    55  				SevenDay: metrics.Counts{Residual: 69},
    56  			},
    57  		},
    58  		TopTestIDs: []analysis.TopCount{{Value: testID, Count: 1}},
    59  	}
    60  }
    61  
    62  func makeReasonCluster(config *compiledcfg.ProjectConfig, uniqifier int) *analysis.Cluster {
    63  	// Because the failure reason clustering algorithm removes numbers
    64  	// when clustering failure reasons, it is better not to use the
    65  	// uniqifier directly in the reason, to avoid cluster ID collisions.
    66  	var foo strings.Builder
    67  	for i := 0; i < uniqifier; i++ {
    68  		foo.WriteString("foo")
    69  	}
    70  	reason := fmt.Sprintf("want %s, got bar", foo.String())
    71  
    72  	return &analysis.Cluster{
    73  		ClusterID: reasonClusterID(config, reason),
    74  		MetricValues: map[metrics.ID]metrics.TimewiseCounts{
    75  			metrics.Failures.ID: {
    76  				OneDay:   metrics.Counts{Residual: 9},
    77  				ThreeDay: metrics.Counts{Residual: 29},
    78  				SevenDay: metrics.Counts{Residual: 69},
    79  			},
    80  		},
    81  		TopTestIDs: []analysis.TopCount{
    82  			{Value: fmt.Sprintf("testname-a-%v", uniqifier), Count: 1},
    83  			{Value: fmt.Sprintf("testname-b-%v", uniqifier), Count: 1},
    84  		},
    85  		ExampleFailureReason: bigquery.NullString{Valid: true, StringVal: reason},
    86  	}
    87  }
    88  
    89  func makeBugCluster(ruleID string) *analysis.Cluster {
    90  	return &analysis.Cluster{
    91  		ClusterID: bugClusterID(ruleID),
    92  		MetricValues: map[metrics.ID]metrics.TimewiseCounts{
    93  			metrics.Failures.ID: {
    94  				OneDay:   metrics.Counts{Residual: 9},
    95  				ThreeDay: metrics.Counts{Residual: 29},
    96  				SevenDay: metrics.Counts{Residual: 69},
    97  			},
    98  		},
    99  		TopTestIDs: []analysis.TopCount{{Value: "testname-0", Count: 1}},
   100  	}
   101  }
   102  
   103  func testIDClusterID(config *compiledcfg.ProjectConfig, testID string) clustering.ClusterID {
   104  	testAlg, err := algorithms.SuggestingAlgorithm(testname.AlgorithmName)
   105  	So(err, ShouldBeNil)
   106  
   107  	return clustering.ClusterID{
   108  		Algorithm: testname.AlgorithmName,
   109  		ID: hex.EncodeToString(testAlg.Cluster(config, &clustering.Failure{
   110  			TestID: testID,
   111  		})),
   112  	}
   113  }
   114  
   115  func reasonClusterID(config *compiledcfg.ProjectConfig, reason string) clustering.ClusterID {
   116  	reasonAlg, err := algorithms.SuggestingAlgorithm(failurereason.AlgorithmName)
   117  	So(err, ShouldBeNil)
   118  
   119  	return clustering.ClusterID{
   120  		Algorithm: failurereason.AlgorithmName,
   121  		ID: hex.EncodeToString(reasonAlg.Cluster(config, &clustering.Failure{
   122  			Reason: &pb.FailureReason{PrimaryErrorMessage: reason},
   123  		})),
   124  	}
   125  }
   126  
   127  func bugClusterID(ruleID string) clustering.ClusterID {
   128  	return clustering.ClusterID{
   129  		Algorithm: rulesalgorithm.AlgorithmName,
   130  		ID:        ruleID,
   131  	}
   132  }
   133  
   134  type fakeAnalysisClient struct {
   135  	clusters []*analysis.Cluster
   136  }
   137  
   138  func (f *fakeAnalysisClient) RebuildAnalysis(ctx context.Context) error {
   139  	return nil
   140  }
   141  
   142  func (f *fakeAnalysisClient) PurgeStaleRows(ctx context.Context) error {
   143  	return nil
   144  }
   145  
   146  func (f *fakeAnalysisClient) ReadImpactfulClusters(ctx context.Context, opts analysis.ImpactfulClusterReadOptions) ([]*analysis.Cluster, error) {
   147  	var results []*analysis.Cluster
   148  	for _, c := range f.clusters {
   149  		include := opts.AlwaysIncludeBugClusters && c.ClusterID.IsBugCluster()
   150  		for _, t := range opts.Thresholds {
   151  			counts, ok := c.MetricValues[metrics.ID(t.MetricId)]
   152  			if !ok {
   153  				continue
   154  			}
   155  			include = include || meetsMetricThreshold(counts, t.Threshold)
   156  		}
   157  		if include {
   158  			results = append(results, c)
   159  		}
   160  	}
   161  	return results, nil
   162  }
   163  
   164  func meetsMetricThreshold(values metrics.TimewiseCounts, threshold *configpb.MetricThreshold) bool {
   165  	return meetsThreshold(values.OneDay.Residual, threshold.OneDay) ||
   166  		meetsThreshold(values.ThreeDay.Residual, threshold.ThreeDay) ||
   167  		meetsThreshold(values.SevenDay.Residual, threshold.SevenDay)
   168  }
   169  
   170  func meetsThreshold(value int64, threshold *int64) bool {
   171  	// threshold == nil is treated as an unsatisfiable threshold.
   172  	return threshold != nil && value >= *threshold
   173  }