go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/clustering/algorithms/failurereason/failurereason_test.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 failurereason
    16  
    17  import (
    18  	"testing"
    19  
    20  	"go.chromium.org/luci/analysis/internal/clustering"
    21  	"go.chromium.org/luci/analysis/internal/clustering/rules/lang"
    22  	"go.chromium.org/luci/analysis/internal/config/compiledcfg"
    23  	configpb "go.chromium.org/luci/analysis/proto/config"
    24  	pb "go.chromium.org/luci/analysis/proto/v1"
    25  
    26  	. "github.com/smartystreets/goconvey/convey"
    27  )
    28  
    29  func TestAlgorithm(t *testing.T) {
    30  	cfgpb := &configpb.ProjectConfig{
    31  		Clustering: &configpb.Clustering{
    32  			ReasonMaskPatterns: []string{
    33  				`(?:^\[Fixture failure\] )([a-zA-Z0-9_]+)[:]`,
    34  			},
    35  		},
    36  	}
    37  	Convey(`Name`, t, func() {
    38  		// Algorithm name should be valid.
    39  		a := &Algorithm{}
    40  		So(clustering.AlgorithmRe.MatchString(a.Name()), ShouldBeTrue)
    41  	})
    42  	Convey(`Cluster`, t, func() {
    43  		a := &Algorithm{}
    44  		cfg, err := compiledcfg.NewConfig(cfgpb)
    45  		So(err, ShouldBeNil)
    46  
    47  		Convey(`Does not cluster test result without failure reason`, func() {
    48  			id := a.Cluster(cfg, &clustering.Failure{})
    49  			So(id, ShouldBeNil)
    50  		})
    51  		Convey(`ID of appropriate length`, func() {
    52  			id := a.Cluster(cfg, &clustering.Failure{
    53  				Reason: &pb.FailureReason{PrimaryErrorMessage: "abcd this is a test failure message"},
    54  			})
    55  			// IDs may be 16 bytes at most.
    56  			So(len(id), ShouldBeGreaterThan, 0)
    57  			So(len(id), ShouldBeLessThanOrEqualTo, clustering.MaxClusterIDBytes)
    58  		})
    59  		Convey(`Same ID for same cluster with different numbers`, func() {
    60  			id1 := a.Cluster(cfg, &clustering.Failure{
    61  				Reason: &pb.FailureReason{PrimaryErrorMessage: "Null pointer exception at ip 0x45637271"},
    62  			})
    63  			id2 := a.Cluster(cfg, &clustering.Failure{
    64  				Reason: &pb.FailureReason{PrimaryErrorMessage: "Null pointer exception at ip 0x12345678"},
    65  			})
    66  			So(id2, ShouldResemble, id1)
    67  		})
    68  		Convey(`Different ID for different clusters`, func() {
    69  			id1 := a.Cluster(cfg, &clustering.Failure{
    70  				Reason: &pb.FailureReason{PrimaryErrorMessage: "Exception in TestMethod"},
    71  			})
    72  			id2 := a.Cluster(cfg, &clustering.Failure{
    73  				Reason: &pb.FailureReason{PrimaryErrorMessage: "Exception in MethodUnderTest"},
    74  			})
    75  			So(id2, ShouldNotResemble, id1)
    76  		})
    77  	})
    78  	Convey(`Failure Association Rule`, t, func() {
    79  		a := &Algorithm{}
    80  		cfg, err := compiledcfg.NewConfig(cfgpb)
    81  		So(err, ShouldBeNil)
    82  
    83  		test := func(failure *clustering.Failure, expectedRule string) {
    84  			rule := a.FailureAssociationRule(cfg, failure)
    85  			So(rule, ShouldEqual, expectedRule)
    86  
    87  			// Test the rule is valid syntax and matches at least the example failure.
    88  			expr, err := lang.Parse(rule)
    89  			So(err, ShouldBeNil)
    90  			So(expr.Evaluate(failure), ShouldBeTrue)
    91  		}
    92  		Convey(`Hexadecimal`, func() {
    93  			failure := &clustering.Failure{
    94  				Reason: &pb.FailureReason{PrimaryErrorMessage: "Null pointer exception at ip 0x45637271"},
    95  			}
    96  			test(failure, `reason LIKE "Null pointer exception at ip %"`)
    97  		})
    98  		Convey(`Numeric`, func() {
    99  			failure := &clustering.Failure{
   100  				Reason: &pb.FailureReason{PrimaryErrorMessage: "Could not connect to 127.1.2.1: connection refused"},
   101  			}
   102  			test(failure, `reason LIKE "Could not connect to %.%.%.%: connection refused"`)
   103  		})
   104  		Convey(`Base64`, func() {
   105  			failure := &clustering.Failure{
   106  				Reason: &pb.FailureReason{PrimaryErrorMessage: "Received unexpected response: AdafdxAAD17917+/="},
   107  			}
   108  			test(failure, `reason LIKE "Received unexpected response: %"`)
   109  		})
   110  		Convey(`Escaping`, func() {
   111  			failure := &clustering.Failure{
   112  				Reason: &pb.FailureReason{PrimaryErrorMessage: `_%"'+[]|` + "\u0000\r\n\v\u202E\u2066 AdafdxAAD17917+/="},
   113  			}
   114  			test(failure, `reason LIKE "\\_\\%\"'+[]|\x00\r\n\v\u202e\u2066 %"`)
   115  		})
   116  		Convey(`Multiline`, func() {
   117  			failure := &clustering.Failure{
   118  				Reason: &pb.FailureReason{
   119  					// Previously "ce\n ... Ac" matched the hexadecimal format
   120  					// for hexadecimal strings of 16 characters or more.
   121  					PrimaryErrorMessage: "Expected: to be called once\n          Actual: never called",
   122  				},
   123  			}
   124  			test(failure, `reason LIKE "Expected: to be called once\n          Actual: never called"`)
   125  		})
   126  		Convey(`User-defined masking`, func() {
   127  			failure := &clustering.Failure{
   128  				Reason: &pb.FailureReason{PrimaryErrorMessage: `[Fixture failure] crostiniBuster: Failed to install Crostini: 3 is not 5`},
   129  			}
   130  			test(failure, `reason LIKE "[Fixture failure] %: Failed to install Crostini: % is not %"`)
   131  		})
   132  	})
   133  	Convey(`Cluster Title`, t, func() {
   134  		a := &Algorithm{}
   135  		cfg, err := compiledcfg.NewConfig(cfgpb)
   136  		So(err, ShouldBeNil)
   137  
   138  		Convey(`Baseline`, func() {
   139  			failure := &clustering.Failure{
   140  				Reason: &pb.FailureReason{PrimaryErrorMessage: "Null pointer exception at ip 0x45637271"},
   141  			}
   142  			title := a.ClusterTitle(cfg, failure)
   143  			So(title, ShouldEqual, `Null pointer exception at ip %`)
   144  		})
   145  		Convey(`Escaping`, func() {
   146  			failure := &clustering.Failure{
   147  				Reason: &pb.FailureReason{PrimaryErrorMessage: `_%"'+[]|` + "\u0000\r\n\v\u202E\u2066 AdafdxAAD17917+/="},
   148  			}
   149  			title := a.ClusterTitle(cfg, failure)
   150  			So(title, ShouldEqual, `_%\"'+[]|\x00\r\n\v\u202e\u2066 %`)
   151  		})
   152  		Convey(`User-defined masking`, func() {
   153  			failure := &clustering.Failure{
   154  				Reason: &pb.FailureReason{PrimaryErrorMessage: `[Fixture failure] crostiniBuster: Failed to install Crostini: 3 is not 5`},
   155  			}
   156  			title := a.ClusterTitle(cfg, failure)
   157  			So(title, ShouldEqual, `[Fixture failure] %: Failed to install Crostini: % is not %`)
   158  		})
   159  	})
   160  	Convey(`Cluster Description`, t, func() {
   161  		a := &Algorithm{}
   162  		cfg, err := compiledcfg.NewConfig(cfgpb)
   163  		So(err, ShouldBeNil)
   164  
   165  		Convey(`Baseline`, func() {
   166  			failure := &clustering.ClusterSummary{
   167  				Example: clustering.Failure{
   168  					Reason: &pb.FailureReason{PrimaryErrorMessage: "Null pointer exception at ip 0x45637271"},
   169  				},
   170  				TopTests: []string{
   171  					"ninja://test_one",
   172  					"ninja://test_two",
   173  					"ninja://test_three",
   174  				},
   175  			}
   176  			description, err := a.ClusterDescription(cfg, failure)
   177  			So(err, ShouldBeNil)
   178  			So(description.Title, ShouldEqual, `Null pointer exception at ip 0x45637271`)
   179  			So(description.Description, ShouldContainSubstring, `Null pointer exception at ip 0x45637271`)
   180  			So(description.Description, ShouldContainSubstring, `- ninja://test_one`)
   181  			So(description.Description, ShouldContainSubstring, `- ninja://test_three`)
   182  			So(description.Description, ShouldContainSubstring, `- ninja://test_three`)
   183  		})
   184  		Convey(`Escaping`, func() {
   185  			summary := &clustering.ClusterSummary{
   186  				Example: clustering.Failure{
   187  					Reason: &pb.FailureReason{PrimaryErrorMessage: `_%"'+[]|` + "\u0000\r\n\v\u202E\u2066 AdafdxAAD17917+/="},
   188  				},
   189  				TopTests: []string{
   190  					"\u2066\u202E\v\n\r\u0000",
   191  				},
   192  			}
   193  			description, err := a.ClusterDescription(cfg, summary)
   194  			So(err, ShouldBeNil)
   195  			So(description.Title, ShouldEqual, `_%\"'+[]|\x00\r\n\v\u202e\u2066 AdafdxAAD17917+/=`)
   196  			So(description.Description, ShouldContainSubstring, `_%\"'+[]|\x00\r\n\v\u202e\u2066 AdafdxAAD17917+/=`)
   197  			So(description.Description, ShouldContainSubstring, `- \u2066\u202e\v\n\r\x00`)
   198  		})
   199  	})
   200  }