go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/pbutil/test_result_test.go (about)

     1  // Copyright 2019 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 pbutil
    16  
    17  import (
    18  	"fmt"
    19  	"regexp"
    20  	"strings"
    21  	"testing"
    22  	"time"
    23  
    24  	. "github.com/smartystreets/goconvey/convey"
    25  	"google.golang.org/protobuf/types/known/durationpb"
    26  	"google.golang.org/protobuf/types/known/structpb"
    27  	"google.golang.org/protobuf/types/known/timestamppb"
    28  
    29  	"go.chromium.org/luci/common/clock/testclock"
    30  	. "go.chromium.org/luci/common/testing/assertions"
    31  	pb "go.chromium.org/luci/resultdb/proto/v1"
    32  )
    33  
    34  // validTestResult returns a valid TestResult sample.
    35  func validTestResult(now time.Time) *pb.TestResult {
    36  	st := timestamppb.New(now.Add(-2 * time.Minute))
    37  	return &pb.TestResult{
    38  		Name:        "invocations/a/tests/invocation_id1/results/result_id1",
    39  		TestId:      "this is testID",
    40  		ResultId:    "result_id1",
    41  		Variant:     Variant("a", "b"),
    42  		Expected:    true,
    43  		Status:      pb.TestStatus_PASS,
    44  		SummaryHtml: "HTML summary",
    45  		StartTime:   st,
    46  		Duration:    durationpb.New(time.Minute),
    47  		TestMetadata: &pb.TestMetadata{
    48  			Location: &pb.TestLocation{
    49  				Repo:     "https://git.example.com",
    50  				FileName: "//a_test.go",
    51  				Line:     54,
    52  			},
    53  			BugComponent: &pb.BugComponent{
    54  				System: &pb.BugComponent_Monorail{
    55  					Monorail: &pb.MonorailComponent{
    56  						Project: "chromium",
    57  						Value:   "Component>Value",
    58  					},
    59  				},
    60  			},
    61  		},
    62  		Tags: StringPairs("k1", "v1"),
    63  	}
    64  }
    65  
    66  // fieldDoesNotMatch returns the string of unspecified error with the field name.
    67  func fieldUnspecified(fieldName string) string {
    68  	return fmt.Sprintf("%s: %s", fieldName, unspecified())
    69  }
    70  
    71  // fieldDoesNotMatch returns the string of doesNotMatch error with the field name.
    72  func fieldDoesNotMatch(fieldName string, re *regexp.Regexp) string {
    73  	return fmt.Sprintf("%s: %s", fieldName, doesNotMatch(re))
    74  }
    75  
    76  func TestTestResultName(t *testing.T) {
    77  	t.Parallel()
    78  
    79  	Convey("ParseTestResultName", t, func() {
    80  		Convey("Parse", func() {
    81  			invID, testID, resultID, err := ParseTestResultName(
    82  				"invocations/a/tests/ninja:%2F%2Fchrome%2Ftest:foo_tests%2FBarTest.DoBaz/results/result5")
    83  			So(err, ShouldBeNil)
    84  			So(invID, ShouldEqual, "a")
    85  			So(testID, ShouldEqual, "ninja://chrome/test:foo_tests/BarTest.DoBaz")
    86  			So(resultID, ShouldEqual, "result5")
    87  		})
    88  
    89  		Convey("Invalid", func() {
    90  			Convey(`has slashes`, func() {
    91  				_, _, _, err := ParseTestResultName(
    92  					"invocations/inv/tests/ninja://test/results/result1")
    93  				So(err, ShouldErrLike, doesNotMatch(testResultNameRe))
    94  			})
    95  
    96  			Convey(`bad unescape`, func() {
    97  				_, _, _, err := ParseTestResultName(
    98  					"invocations/a/tests/bad_hex_%gg/results/result1")
    99  				So(err, ShouldErrLike, "test id")
   100  			})
   101  
   102  			Convey(`unescaped unprintable`, func() {
   103  				_, _, _, err := ParseTestResultName(
   104  					"invocations/a/tests/unprintable_%07/results/result1")
   105  				So(err, ShouldErrLike, "non-printable rune")
   106  			})
   107  		})
   108  
   109  		Convey("Format", func() {
   110  			So(TestResultName("a", "ninja://chrome/test:foo_tests/BarTest.DoBaz", "result5"),
   111  				ShouldEqual,
   112  				"invocations/a/tests/ninja:%2F%2Fchrome%2Ftest:foo_tests%2FBarTest.DoBaz/results/result5")
   113  		})
   114  	})
   115  }
   116  
   117  func TestValidateTestResult(t *testing.T) {
   118  	t.Parallel()
   119  	now := testclock.TestRecentTimeUTC
   120  	validate := func(result *pb.TestResult) error {
   121  		return ValidateTestResult(now, result)
   122  	}
   123  
   124  	Convey("Succeeds", t, func() {
   125  		msg := validTestResult(now)
   126  		So(validate(msg), ShouldBeNil)
   127  
   128  		Convey("with unicode TestID", func() {
   129  			// Uses printable unicode character 'µ'.
   130  			msg.TestId = "TestVariousDeadlines/5µs"
   131  			So(ValidateTestID(msg.TestId), ShouldErrLike, nil)
   132  			So(validate(msg), ShouldBeNil)
   133  		})
   134  
   135  		Convey("with invalid Name", func() {
   136  			// ValidateTestResult should skip validating TestResult.Name.
   137  			msg.Name = "this is not a valid name for TestResult.Name"
   138  			So(ValidateTestResultName(msg.Name), ShouldErrLike, doesNotMatch(testResultNameRe))
   139  			So(validate(msg), ShouldBeNil)
   140  		})
   141  
   142  		Convey("with no variant", func() {
   143  			msg.Variant = nil
   144  			So(validate(msg), ShouldBeNil)
   145  		})
   146  
   147  		Convey("with valid summary", func() {
   148  			msg.SummaryHtml = strings.Repeat("1", maxLenSummaryHTML)
   149  			So(validate(msg), ShouldBeNil)
   150  		})
   151  
   152  		Convey("with empty tags", func() {
   153  			msg.Tags = nil
   154  			So(validate(msg), ShouldBeNil)
   155  		})
   156  
   157  		Convey("with nil start_time", func() {
   158  			msg.StartTime = nil
   159  			So(validate(msg), ShouldBeNil)
   160  		})
   161  
   162  		Convey("with nil duration", func() {
   163  			msg.Duration = nil
   164  			So(validate(msg), ShouldBeNil)
   165  		})
   166  
   167  		Convey("with valid properties", func() {
   168  			msg.Properties = &structpb.Struct{
   169  				Fields: map[string]*structpb.Value{
   170  					"key": structpb.NewStringValue("value"),
   171  				},
   172  			}
   173  			So(validate(msg), ShouldBeNil)
   174  		})
   175  
   176  		Convey("with skip reason", func() {
   177  			msg.Status = pb.TestStatus_SKIP
   178  			msg.SkipReason = pb.SkipReason_AUTOMATICALLY_DISABLED_FOR_FLAKINESS
   179  			So(validate(msg), ShouldBeNil)
   180  		})
   181  	})
   182  
   183  	Convey("Fails", t, func() {
   184  		msg := validTestResult(now)
   185  		Convey("with nil", func() {
   186  			So(validate(nil), ShouldErrLike, unspecified())
   187  		})
   188  
   189  		Convey("with empty TestID", func() {
   190  			msg.TestId = ""
   191  			So(validate(msg), ShouldErrLike, fieldUnspecified("test_id"))
   192  		})
   193  
   194  		Convey("with invalid TestID", func() {
   195  			badInputs := []string{
   196  				strings.Repeat("1", 512+1),
   197  				// [[:print:]] matches with [ -~] and [[:graph:]]
   198  				string(rune(7)),
   199  				string("cafe\u0301"), // UTF8 text that is not in normalization form C.
   200  			}
   201  			for _, in := range badInputs {
   202  				msg.TestId = in
   203  				So(validate(msg), ShouldErrLike, "")
   204  			}
   205  		})
   206  
   207  		Convey("with empty ResultID", func() {
   208  			msg.ResultId = ""
   209  			So(validate(msg), ShouldErrLike, fieldUnspecified("result_id"))
   210  		})
   211  
   212  		Convey("with invalid ResultID", func() {
   213  			badInputs := []string{
   214  				strings.Repeat("1", 32+1),
   215  				string(rune(7)),
   216  			}
   217  			for _, in := range badInputs {
   218  				msg.ResultId = in
   219  				So(validate(msg), ShouldErrLike, fieldDoesNotMatch("result_id", resultIDRe))
   220  			}
   221  		})
   222  
   223  		Convey("with invalid Variant", func() {
   224  			badInputs := []*pb.Variant{
   225  				Variant("", ""),
   226  				Variant("", "val"),
   227  			}
   228  			for _, in := range badInputs {
   229  				msg.Variant = in
   230  				So(validate(msg), ShouldErrLike, fieldUnspecified("key"))
   231  			}
   232  		})
   233  
   234  		Convey("with invalid Status", func() {
   235  			msg.Status = pb.TestStatus(len(pb.TestStatus_name) + 1)
   236  			So(validate(msg), ShouldErrLike, "status: invalid value")
   237  		})
   238  
   239  		Convey("with STATUS_UNSPECIFIED", func() {
   240  			msg.Status = pb.TestStatus_STATUS_UNSPECIFIED
   241  			So(validate(msg), ShouldErrLike, "status: cannot be STATUS_UNSPECIFIED")
   242  		})
   243  
   244  		Convey("with skip reason but not skip status", func() {
   245  			msg.Status = pb.TestStatus_ABORT
   246  			msg.SkipReason = pb.SkipReason_AUTOMATICALLY_DISABLED_FOR_FLAKINESS
   247  			So(validate(msg), ShouldErrLike, "skip_reason: value must be zero (UNSPECIFIED) when status is not SKIP")
   248  		})
   249  
   250  		Convey("with too big summary", func() {
   251  			msg.SummaryHtml = strings.Repeat("☕", maxLenSummaryHTML)
   252  			So(validate(msg), ShouldErrLike, "summary_html: exceeds the maximum size")
   253  		})
   254  
   255  		Convey("with invalid StartTime and Duration", func() {
   256  			Convey("because start_time is in the future", func() {
   257  				future := timestamppb.New(now.Add(time.Hour))
   258  				msg.StartTime = future
   259  				So(validate(msg), ShouldErrLike, fmt.Sprintf("start_time: cannot be > (now + %s)", clockSkew))
   260  			})
   261  
   262  			Convey("because duration is < 0", func() {
   263  				msg.Duration = durationpb.New(-1 * time.Minute)
   264  				So(validate(msg), ShouldErrLike, "duration: is < 0")
   265  			})
   266  
   267  			Convey("because (start_time + duration) is in the future", func() {
   268  				st := timestamppb.New(now.Add(-1 * time.Hour))
   269  				msg.StartTime = st
   270  				msg.Duration = durationpb.New(2 * time.Hour)
   271  				expected := fmt.Sprintf("start_time + duration: cannot be > (now + %s)", clockSkew)
   272  				So(validate(msg), ShouldErrLike, expected)
   273  			})
   274  		})
   275  
   276  		Convey("with invalid StringPairs", func() {
   277  			msg.Tags = StringPairs("", "")
   278  			So(validate(msg), ShouldErrLike, `"":"": key: unspecified`)
   279  		})
   280  
   281  		Convey("Test metadata", func() {
   282  			Convey("filename", func() {
   283  				Convey("unspecified", func() {
   284  					msg.TestMetadata.Location.FileName = ""
   285  					So(validate(msg), ShouldErrLike, "test_metadata: location: file_name: unspecified")
   286  				})
   287  				Convey("too long", func() {
   288  					msg.TestMetadata.Location.FileName = "//" + strings.Repeat("super long", 100)
   289  					So(validate(msg), ShouldErrLike, "test_metadata: location: file_name: length exceeds 512")
   290  				})
   291  				Convey("no double slashes", func() {
   292  					msg.TestMetadata.Location.FileName = "file_name"
   293  					So(validate(msg), ShouldErrLike, "test_metadata: location: file_name: doesn't start with //")
   294  				})
   295  				Convey("back slash", func() {
   296  					msg.TestMetadata.Location.FileName = "//dir\\file"
   297  					So(validate(msg), ShouldErrLike, "test_metadata: location: file_name: has \\")
   298  				})
   299  				Convey("trailing slash", func() {
   300  					msg.TestMetadata.Location.FileName = "//file_name/"
   301  					So(validate(msg), ShouldErrLike, "test_metadata: location: file_name: ends with /")
   302  				})
   303  			})
   304  			Convey("line", func() {
   305  				msg.TestMetadata.Location.Line = -1
   306  				So(validate(msg), ShouldErrLike, "test_metadata: location: line: must not be negative")
   307  			})
   308  			Convey("repo", func() {
   309  				msg.TestMetadata.Location.Repo = "https://chromium.googlesource.com/chromium/src.git"
   310  				So(validate(msg), ShouldErrLike, "test_metadata: location: repo: must not end with .git")
   311  			})
   312  
   313  			Convey("no location and no bug component", func() {
   314  				msg.TestMetadata = &pb.TestMetadata{Name: "name"}
   315  				So(validate(msg), ShouldBeNil)
   316  			})
   317  			Convey("location no repo", func() {
   318  				msg.TestMetadata = &pb.TestMetadata{
   319  					Name: "name",
   320  					Location: &pb.TestLocation{
   321  						FileName: "//file_name",
   322  					},
   323  				}
   324  				So(validate(msg), ShouldErrLike, "test_metadata: location: repo: required")
   325  			})
   326  
   327  			Convey("nil bug system in bug component", func() {
   328  				msg.TestMetadata = &pb.TestMetadata{
   329  					Name: "name",
   330  					BugComponent: &pb.BugComponent{
   331  						System: nil,
   332  					},
   333  				}
   334  				So(validate(msg), ShouldErrLike, "bug system is required for bug components")
   335  			})
   336  			Convey("valid monorail bug component", func() {
   337  				msg.TestMetadata = &pb.TestMetadata{
   338  					Name: "name",
   339  					BugComponent: &pb.BugComponent{
   340  						System: &pb.BugComponent_Monorail{
   341  							Monorail: &pb.MonorailComponent{
   342  								Project: "1chromium1",
   343  								Value:   "Component>Value",
   344  							},
   345  						},
   346  					},
   347  				}
   348  				So(validate(msg), ShouldBeNil)
   349  			})
   350  			Convey("wrong size monorail bug component value", func() {
   351  				msg.TestMetadata = &pb.TestMetadata{
   352  					Name: "name",
   353  					BugComponent: &pb.BugComponent{
   354  						System: &pb.BugComponent_Monorail{
   355  							Monorail: &pb.MonorailComponent{
   356  								Project: "chromium",
   357  								Value:   strings.Repeat("a", 601),
   358  							},
   359  						},
   360  					},
   361  				}
   362  				So(validate(msg), ShouldErrLike, "monorail.value: is invalid")
   363  			})
   364  			Convey("invalid monorail bug component value", func() {
   365  				msg.TestMetadata = &pb.TestMetadata{
   366  					Name: "name",
   367  					BugComponent: &pb.BugComponent{
   368  						System: &pb.BugComponent_Monorail{
   369  							Monorail: &pb.MonorailComponent{
   370  								Project: "chromium",
   371  								Value:   "Component<><>Value",
   372  							},
   373  						},
   374  					},
   375  				}
   376  				So(validate(msg), ShouldErrLike, "monorail.value: is invalid")
   377  			})
   378  			Convey("wrong size monorail bug component project", func() {
   379  				msg.TestMetadata = &pb.TestMetadata{
   380  					Name: "name",
   381  					BugComponent: &pb.BugComponent{
   382  						System: &pb.BugComponent_Monorail{
   383  							Monorail: &pb.MonorailComponent{
   384  								Project: strings.Repeat("a", 64),
   385  								Value:   "Component>Value",
   386  							},
   387  						},
   388  					},
   389  				}
   390  				So(validate(msg), ShouldErrLike, "monorail.project: is invalid")
   391  			})
   392  			Convey("using invalid characters in monorail bug component project", func() {
   393  				msg.TestMetadata = &pb.TestMetadata{
   394  					Name: "name",
   395  					BugComponent: &pb.BugComponent{
   396  						System: &pb.BugComponent_Monorail{
   397  							Monorail: &pb.MonorailComponent{
   398  								Project: "$%^ $$^%",
   399  								Value:   "Component>Value",
   400  							},
   401  						},
   402  					},
   403  				}
   404  				So(validate(msg), ShouldErrLike, "monorail.project: is invalid")
   405  			})
   406  			Convey("using only numbers in monorail bug component project", func() {
   407  				msg.TestMetadata = &pb.TestMetadata{
   408  					Name: "name",
   409  					BugComponent: &pb.BugComponent{
   410  						System: &pb.BugComponent_Monorail{
   411  							Monorail: &pb.MonorailComponent{
   412  								Project: "11111",
   413  								Value:   "Component>Value",
   414  							},
   415  						},
   416  					},
   417  				}
   418  				So(validate(msg), ShouldErrLike, "monorail.project: is invalid")
   419  			})
   420  			Convey("valid buganizer component", func() {
   421  				msg.TestMetadata = &pb.TestMetadata{
   422  					Name: "name",
   423  					BugComponent: &pb.BugComponent{
   424  						System: &pb.BugComponent_IssueTracker{
   425  							IssueTracker: &pb.IssueTrackerComponent{
   426  								ComponentId: 1234,
   427  							},
   428  						},
   429  					},
   430  				}
   431  				So(validate(msg), ShouldBeNil)
   432  			})
   433  			Convey("invalid buganizer component id", func() {
   434  				msg.TestMetadata = &pb.TestMetadata{
   435  					Name: "name",
   436  					BugComponent: &pb.BugComponent{
   437  						System: &pb.BugComponent_IssueTracker{
   438  							IssueTracker: &pb.IssueTrackerComponent{
   439  								ComponentId: -1,
   440  							},
   441  						},
   442  					},
   443  				}
   444  				So(validate(msg), ShouldErrLike, "issue_tracker.component_id: is invalid")
   445  			})
   446  			Convey("with too big properties", func() {
   447  				msg.TestMetadata = &pb.TestMetadata{
   448  					PropertiesSchema: "package.message",
   449  					Properties: &structpb.Struct{
   450  						Fields: map[string]*structpb.Value{
   451  							"key": structpb.NewStringValue(strings.Repeat("1", MaxSizeProperties)),
   452  						},
   453  					},
   454  				}
   455  				So(validate(msg), ShouldErrLike, "properties: exceeds the maximum size")
   456  			})
   457  			Convey("no properties_schema with non-empty properties", func() {
   458  				msg.TestMetadata = &pb.TestMetadata{
   459  					Properties: &structpb.Struct{
   460  						Fields: map[string]*structpb.Value{
   461  							"key": structpb.NewStringValue("1"),
   462  						},
   463  					},
   464  				}
   465  				So(validate(msg), ShouldErrLike, "properties_schema must be specified with non-empty properties")
   466  			})
   467  			Convey("invalid properties_schema", func() {
   468  				msg.TestMetadata = &pb.TestMetadata{
   469  					PropertiesSchema: "package",
   470  				}
   471  				So(validate(msg), ShouldErrLike, "properties_schema: does not match")
   472  			})
   473  			Convey("valid properties_schema and non-empty properties", func() {
   474  				msg.TestMetadata = &pb.TestMetadata{
   475  					PropertiesSchema: "package.message",
   476  					Properties: &structpb.Struct{
   477  						Fields: map[string]*structpb.Value{
   478  							"key": structpb.NewStringValue("1"),
   479  						},
   480  					},
   481  				}
   482  				So(validate(msg), ShouldBeNil)
   483  			})
   484  		})
   485  
   486  		Convey("with too big properties", func() {
   487  			msg.Properties = &structpb.Struct{
   488  				Fields: map[string]*structpb.Value{
   489  					"key": structpb.NewStringValue(strings.Repeat("1", MaxSizeProperties)),
   490  				},
   491  			}
   492  			So(validate(msg), ShouldErrLike, "properties: exceeds the maximum size")
   493  		})
   494  
   495  		Convey("Validate failure reason", func() {
   496  			errorMessage1 := "error1"
   497  			errorMessage2 := "error2"
   498  			longErrorMessage := strings.Repeat("a very long error message", 100)
   499  			Convey("valid failure reason", func() {
   500  				msg.FailureReason = &pb.FailureReason{
   501  					PrimaryErrorMessage: errorMessage1,
   502  					Errors: []*pb.FailureReason_Error{
   503  						{Message: errorMessage1},
   504  						{Message: errorMessage2},
   505  					},
   506  					TruncatedErrorsCount: 0,
   507  				}
   508  				So(validate(msg), ShouldBeNil)
   509  			})
   510  
   511  			Convey("primary_error_message exceeds the maximum limit", func() {
   512  				msg.FailureReason = &pb.FailureReason{
   513  					PrimaryErrorMessage: longErrorMessage,
   514  				}
   515  				So(validate(msg), ShouldErrLike, "primary_error_message: "+
   516  					"exceeds the maximum")
   517  			})
   518  
   519  			Convey("one of the error messages exceeds the maximum limit", func() {
   520  				msg.FailureReason = &pb.FailureReason{
   521  					PrimaryErrorMessage: errorMessage1,
   522  					Errors: []*pb.FailureReason_Error{
   523  						{Message: errorMessage1},
   524  						{Message: longErrorMessage},
   525  					},
   526  					TruncatedErrorsCount: 0,
   527  				}
   528  				So(validate(msg), ShouldErrLike,
   529  					"errors[1]: message: exceeds the maximum size of 1024 "+
   530  						"bytes")
   531  			})
   532  
   533  			Convey("the first error doesn't match primary_error_message", func() {
   534  				msg.FailureReason = &pb.FailureReason{
   535  					PrimaryErrorMessage: errorMessage1,
   536  					Errors: []*pb.FailureReason_Error{
   537  						{Message: errorMessage2},
   538  					},
   539  					TruncatedErrorsCount: 0,
   540  				}
   541  				So(validate(msg), ShouldErrLike,
   542  					"errors[0]: message: must match primary_error_message")
   543  			})
   544  
   545  			Convey("the total size of the errors list exceeds the limit", func() {
   546  				maxErrorMessage := strings.Repeat(".", 1024)
   547  				msg.FailureReason = &pb.FailureReason{
   548  					PrimaryErrorMessage: maxErrorMessage,
   549  					Errors: []*pb.FailureReason_Error{
   550  						{Message: maxErrorMessage},
   551  						{Message: maxErrorMessage},
   552  						{Message: maxErrorMessage},
   553  						{Message: maxErrorMessage},
   554  					},
   555  					TruncatedErrorsCount: 1,
   556  				}
   557  				So(validate(msg), ShouldErrLike,
   558  					"errors: exceeds the maximum total size of 3172 bytes")
   559  			})
   560  
   561  			Convey("invalid truncated error count", func() {
   562  				msg.FailureReason = &pb.FailureReason{
   563  					PrimaryErrorMessage: errorMessage1,
   564  					Errors: []*pb.FailureReason_Error{
   565  						{Message: errorMessage1},
   566  						{Message: errorMessage2},
   567  					},
   568  					TruncatedErrorsCount: -1,
   569  				}
   570  				So(validate(msg), ShouldErrLike, "truncated_errors_count: "+
   571  					"must be non-negative")
   572  			})
   573  		})
   574  	})
   575  }