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 }