go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/testresults/query_test.go (about)

     1  // Copyright 2020 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 testresults
    16  
    17  import (
    18  	"context"
    19  	"sort"
    20  	"strings"
    21  	"testing"
    22  	"time"
    23  
    24  	. "github.com/smartystreets/goconvey/convey"
    25  	"google.golang.org/grpc/codes"
    26  	durpb "google.golang.org/protobuf/types/known/durationpb"
    27  	"google.golang.org/protobuf/types/known/structpb"
    28  	"google.golang.org/protobuf/types/known/timestamppb"
    29  
    30  	"go.chromium.org/luci/common/clock/testclock"
    31  	"go.chromium.org/luci/common/proto/mask"
    32  	. "go.chromium.org/luci/common/testing/assertions"
    33  	"go.chromium.org/luci/resultdb/internal/invocations"
    34  	"go.chromium.org/luci/resultdb/internal/testutil"
    35  	"go.chromium.org/luci/resultdb/internal/testutil/insert"
    36  	"go.chromium.org/luci/resultdb/pbutil"
    37  	pb "go.chromium.org/luci/resultdb/proto/v1"
    38  	"go.chromium.org/luci/server/span"
    39  )
    40  
    41  func TestQueryTestResults(t *testing.T) {
    42  	Convey(`QueryTestResults`, t, func() {
    43  		ctx := testutil.SpannerTestContext(t)
    44  
    45  		testutil.MustApply(ctx, insert.Invocation("inv1", pb.Invocation_ACTIVE, map[string]any{
    46  			"CommonTestIDPrefix":     "",
    47  			"TestResultVariantUnion": []string{"a:b", "k:1", "k:2", "k2:1"},
    48  		}))
    49  
    50  		q := &Query{
    51  			Predicate:     &pb.TestResultPredicate{},
    52  			PageSize:      100,
    53  			InvocationIDs: invocations.NewIDSet("inv1"),
    54  			Mask:          AllFields,
    55  		}
    56  
    57  		fetch := func(q *Query) (trs []*pb.TestResult, token string, err error) {
    58  			ctx, cancel := span.ReadOnlyTransaction(ctx)
    59  			defer cancel()
    60  			return q.Fetch(ctx)
    61  		}
    62  
    63  		mustFetch := func(q *Query) (trs []*pb.TestResult, token string) {
    64  			trs, token, err := fetch(q)
    65  			So(err, ShouldBeNil)
    66  			return
    67  		}
    68  
    69  		mustFetchNames := func(q *Query) []string {
    70  			trs, _, err := fetch(q)
    71  			So(err, ShouldBeNil)
    72  			names := make([]string, len(trs))
    73  			for i, a := range trs {
    74  				names[i] = a.Name
    75  			}
    76  			sort.Strings(names)
    77  			return names
    78  		}
    79  
    80  		Convey(`Does not fetch test results of other invocations`, func() {
    81  			expected := insert.MakeTestResults("inv1", "DoBaz", nil,
    82  				pb.TestStatus_PASS,
    83  				pb.TestStatus_FAIL,
    84  				pb.TestStatus_FAIL,
    85  				pb.TestStatus_PASS,
    86  				pb.TestStatus_FAIL,
    87  			)
    88  			testutil.MustApply(ctx,
    89  				insert.Invocation("inv0", pb.Invocation_ACTIVE, nil),
    90  				insert.Invocation("inv2", pb.Invocation_ACTIVE, nil),
    91  			)
    92  			testutil.MustApply(ctx, testutil.CombineMutations(
    93  				insert.TestResults("inv0", "X", nil, pb.TestStatus_PASS, pb.TestStatus_FAIL),
    94  				insert.TestResultMessages(expected),
    95  				insert.TestResults("inv2", "Y", nil, pb.TestStatus_PASS, pb.TestStatus_FAIL),
    96  			)...)
    97  
    98  			actual, _ := mustFetch(q)
    99  			So(actual, ShouldResembleProto, expected)
   100  		})
   101  
   102  		Convey(`Expectancy filter`, func() {
   103  			testutil.MustApply(ctx, insert.Invocation("inv0", pb.Invocation_ACTIVE, map[string]any{
   104  				"CommonTestIDPrefix":     "",
   105  				"TestResultVariantUnion": pbutil.Variant("a", "b"),
   106  			}))
   107  			q.InvocationIDs = invocations.NewIDSet("inv0", "inv1")
   108  
   109  			Convey(`VARIANTS_WITH_UNEXPECTED_RESULTS`, func() {
   110  				q.Predicate.Expectancy = pb.TestResultPredicate_VARIANTS_WITH_UNEXPECTED_RESULTS
   111  
   112  				testutil.MustApply(ctx, testutil.CombineMutations(
   113  					insert.TestResults("inv0", "T1", nil, pb.TestStatus_PASS, pb.TestStatus_FAIL),
   114  					insert.TestResults("inv0", "T2", nil, pb.TestStatus_PASS),
   115  					insert.TestResults("inv1", "T1", nil, pb.TestStatus_PASS),
   116  					insert.TestResults("inv1", "T2", nil, pb.TestStatus_FAIL),
   117  					insert.TestResults("inv1", "T3", nil, pb.TestStatus_PASS),
   118  					insert.TestResults("inv1", "T4", pbutil.Variant("a", "b"), pb.TestStatus_FAIL),
   119  					insert.TestExonerations("inv0", "T1", nil, pb.ExonerationReason_OCCURS_ON_OTHER_CLS),
   120  				)...)
   121  
   122  				Convey(`Works`, func() {
   123  					So(mustFetchNames(q), ShouldResemble, []string{
   124  						"invocations/inv0/tests/T1/results/0",
   125  						"invocations/inv0/tests/T1/results/1",
   126  						"invocations/inv0/tests/T2/results/0",
   127  						"invocations/inv1/tests/T1/results/0",
   128  						"invocations/inv1/tests/T2/results/0",
   129  						"invocations/inv1/tests/T4/results/0",
   130  					})
   131  				})
   132  
   133  				Convey(`TestID filter`, func() {
   134  					q.Predicate.TestIdRegexp = ".*T4"
   135  					So(mustFetchNames(q), ShouldResemble, []string{
   136  						"invocations/inv1/tests/T4/results/0",
   137  					})
   138  				})
   139  
   140  				Convey(`Variant filter`, func() {
   141  					q.Predicate.Variant = &pb.VariantPredicate{
   142  						Predicate: &pb.VariantPredicate_Equals{
   143  							Equals: pbutil.Variant("a", "b"),
   144  						},
   145  					}
   146  					So(mustFetchNames(q), ShouldResemble, []string{
   147  						"invocations/inv1/tests/T4/results/0",
   148  					})
   149  				})
   150  
   151  				Convey(`ExcludeExonerated`, func() {
   152  					q.Predicate.ExcludeExonerated = true
   153  					So(mustFetchNames(q), ShouldResemble, []string{
   154  						"invocations/inv0/tests/T2/results/0",
   155  						"invocations/inv1/tests/T2/results/0",
   156  						"invocations/inv1/tests/T4/results/0",
   157  					})
   158  				})
   159  			})
   160  
   161  			Convey(`VARIANTS_WITH_ONLY_UNEXPECTED_RESULTS`, func() {
   162  				q.Predicate.Expectancy = pb.TestResultPredicate_VARIANTS_WITH_ONLY_UNEXPECTED_RESULTS
   163  
   164  				testutil.MustApply(ctx, testutil.CombineMutations(
   165  					insert.TestResults("inv0", "flaky", nil, pb.TestStatus_PASS, pb.TestStatus_FAIL),
   166  					insert.TestResults("inv0", "passing", nil, pb.TestStatus_PASS),
   167  					insert.TestResults("inv0", "F0", pbutil.Variant("a", "0"), pb.TestStatus_FAIL),
   168  					insert.TestResults("inv0", "in_both_invocations", nil, pb.TestStatus_FAIL),
   169  					// Same test, but different variant.
   170  					insert.TestResults("inv1", "F0", pbutil.Variant("a", "1"), pb.TestStatus_PASS),
   171  					insert.TestResults("inv1", "in_both_invocations", nil, pb.TestStatus_PASS),
   172  					insert.TestResults("inv1", "F1", nil, pb.TestStatus_FAIL, pb.TestStatus_FAIL),
   173  				)...)
   174  
   175  				Convey(`Works`, func() {
   176  					So(mustFetchNames(q), ShouldResemble, []string{
   177  						"invocations/inv0/tests/F0/results/0",
   178  						"invocations/inv1/tests/F1/results/0",
   179  						"invocations/inv1/tests/F1/results/1",
   180  					})
   181  				})
   182  			})
   183  		})
   184  
   185  		Convey(`Test id filter`, func() {
   186  			testutil.MustApply(ctx,
   187  				insert.Invocation("inv0", pb.Invocation_ACTIVE, map[string]any{
   188  					"CommonTestIDPrefix": "1-",
   189  				}),
   190  				insert.Invocation("inv2", pb.Invocation_ACTIVE, map[string]any{
   191  					"CreateTime": time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
   192  				}),
   193  			)
   194  			testutil.MustApply(ctx, testutil.CombineMutations(
   195  				insert.TestResults("inv0", "1-1", nil, pb.TestStatus_PASS, pb.TestStatus_FAIL),
   196  				insert.TestResults("inv0", "1-2", nil, pb.TestStatus_PASS),
   197  				insert.TestResults("inv1", "1-1", nil, pb.TestStatus_PASS),
   198  				insert.TestResults("inv1", "2-1", nil, pb.TestStatus_PASS),
   199  				insert.TestResults("inv1", "2", nil, pb.TestStatus_FAIL),
   200  				insert.TestResults("inv2", "1-2", nil, pb.TestStatus_PASS),
   201  			)...)
   202  
   203  			q.InvocationIDs = invocations.NewIDSet("inv0", "inv1", "inv2")
   204  			q.Predicate.TestIdRegexp = "1-.+"
   205  
   206  			So(mustFetchNames(q), ShouldResemble, []string{
   207  				"invocations/inv0/tests/1-1/results/0",
   208  				"invocations/inv0/tests/1-1/results/1",
   209  				"invocations/inv0/tests/1-2/results/0",
   210  				"invocations/inv1/tests/1-1/results/0",
   211  				"invocations/inv2/tests/1-2/results/0",
   212  			})
   213  		})
   214  
   215  		Convey(`Variant equals`, func() {
   216  			v1 := pbutil.Variant("k", "1")
   217  			v2 := pbutil.Variant("k", "2")
   218  			testutil.MustApply(ctx,
   219  				insert.Invocation("inv0", pb.Invocation_ACTIVE, map[string]any{
   220  					"TestResultVariantUnion": []string{"k:1", "k:2"},
   221  				}),
   222  				insert.Invocation("inv2", pb.Invocation_ACTIVE, map[string]any{
   223  					"CreateTime": time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
   224  				}),
   225  			)
   226  			testutil.MustApply(ctx, testutil.CombineMutations(
   227  				insert.TestResults("inv0", "1-1", v1, pb.TestStatus_PASS, pb.TestStatus_FAIL),
   228  				insert.TestResults("inv0", "1-2", v2, pb.TestStatus_PASS),
   229  				insert.TestResults("inv1", "1-1", v1, pb.TestStatus_PASS),
   230  				insert.TestResults("inv1", "2-1", v2, pb.TestStatus_PASS),
   231  				insert.TestResults("inv2", "1-1", v1, pb.TestStatus_PASS),
   232  			)...)
   233  
   234  			q.InvocationIDs = invocations.NewIDSet("inv0", "inv1", "inv2")
   235  			q.Predicate.Variant = &pb.VariantPredicate{
   236  				Predicate: &pb.VariantPredicate_Equals{Equals: v1},
   237  			}
   238  
   239  			So(mustFetchNames(q), ShouldResemble, []string{
   240  				"invocations/inv0/tests/1-1/results/0",
   241  				"invocations/inv0/tests/1-1/results/1",
   242  				"invocations/inv1/tests/1-1/results/0",
   243  				"invocations/inv2/tests/1-1/results/0",
   244  			})
   245  		})
   246  
   247  		Convey(`Variant contains`, func() {
   248  			testutil.MustApply(ctx, insert.Invocation("inv0", pb.Invocation_ACTIVE, map[string]any{
   249  				"TestResultVariantUnion": []string{"k:1", "k:2", "k2:1"},
   250  			}))
   251  
   252  			v1 := pbutil.Variant("k", "1")
   253  			v11 := pbutil.Variant("k", "1", "k2", "1")
   254  			v2 := pbutil.Variant("k", "2")
   255  			testutil.MustApply(ctx, testutil.CombineMutations(
   256  				insert.TestResults("inv0", "1-1", v1, pb.TestStatus_PASS, pb.TestStatus_FAIL),
   257  				insert.TestResults("inv0", "1-2", v11, pb.TestStatus_PASS),
   258  				insert.TestResults("inv1", "1-1", v1, pb.TestStatus_PASS),
   259  				insert.TestResults("inv1", "2-1", v2, pb.TestStatus_PASS),
   260  			)...)
   261  
   262  			q.InvocationIDs = invocations.NewIDSet("inv0", "inv1")
   263  
   264  			Convey(`Empty`, func() {
   265  				q.Predicate.Variant = &pb.VariantPredicate{
   266  					Predicate: &pb.VariantPredicate_Contains{Contains: pbutil.Variant()},
   267  				}
   268  
   269  				So(mustFetchNames(q), ShouldResemble, []string{
   270  					"invocations/inv0/tests/1-1/results/0",
   271  					"invocations/inv0/tests/1-1/results/1",
   272  					"invocations/inv0/tests/1-2/results/0",
   273  					"invocations/inv1/tests/1-1/results/0",
   274  					"invocations/inv1/tests/2-1/results/0",
   275  				})
   276  			})
   277  
   278  			Convey(`Non-empty`, func() {
   279  				q.Predicate.Variant = &pb.VariantPredicate{
   280  					Predicate: &pb.VariantPredicate_Contains{Contains: v1},
   281  				}
   282  
   283  				So(mustFetchNames(q), ShouldResemble, []string{
   284  					"invocations/inv0/tests/1-1/results/0",
   285  					"invocations/inv0/tests/1-1/results/1",
   286  					"invocations/inv0/tests/1-2/results/0",
   287  					"invocations/inv1/tests/1-1/results/0",
   288  				})
   289  			})
   290  		})
   291  
   292  		Convey(`Paging`, func() {
   293  			trs := insert.MakeTestResults("inv1", "DoBaz", nil,
   294  				pb.TestStatus_PASS,
   295  				pb.TestStatus_FAIL,
   296  				pb.TestStatus_FAIL,
   297  				pb.TestStatus_PASS,
   298  				pb.TestStatus_FAIL,
   299  			)
   300  			testutil.MustApply(ctx, insert.TestResultMessages(trs)...)
   301  
   302  			mustReadPage := func(pageToken string, pageSize int, expected []*pb.TestResult) string {
   303  				q2 := q
   304  				q2.PageToken = pageToken
   305  				q2.PageSize = pageSize
   306  				actual, token := mustFetch(q2)
   307  				So(actual, ShouldResembleProto, expected)
   308  				return token
   309  			}
   310  
   311  			Convey(`All results`, func() {
   312  				token := mustReadPage("", 10, trs)
   313  				So(token, ShouldEqual, "")
   314  			})
   315  
   316  			Convey(`With pagination`, func() {
   317  				token := mustReadPage("", 1, trs[:1])
   318  				So(token, ShouldNotEqual, "")
   319  
   320  				token = mustReadPage(token, 4, trs[1:])
   321  				So(token, ShouldNotEqual, "")
   322  
   323  				token = mustReadPage(token, 5, nil)
   324  				So(token, ShouldEqual, "")
   325  			})
   326  
   327  			Convey(`Bad token`, func() {
   328  				ctx, cancel := span.ReadOnlyTransaction(ctx)
   329  				defer cancel()
   330  
   331  				Convey(`From bad position`, func() {
   332  					q.PageToken = "CgVoZWxsbw=="
   333  					_, _, err := q.Fetch(ctx)
   334  					So(err, ShouldHaveAppStatus, codes.InvalidArgument, "invalid page_token")
   335  				})
   336  
   337  				Convey(`From decoding`, func() {
   338  					q.PageToken = "%%%"
   339  					_, _, err := q.Fetch(ctx)
   340  					So(err, ShouldHaveAppStatus, codes.InvalidArgument, "invalid page_token")
   341  				})
   342  			})
   343  		})
   344  
   345  		Convey(`Test metadata`, func() {
   346  			expected := insert.MakeTestResults("inv1", "DoBaz", nil, pb.TestStatus_PASS)
   347  			expected[0].TestMetadata = &pb.TestMetadata{
   348  				Name: "original_name",
   349  				Location: &pb.TestLocation{
   350  					FileName: "//a_test.go",
   351  					Line:     54,
   352  				},
   353  				BugComponent: &pb.BugComponent{
   354  					System: &pb.BugComponent_Monorail{
   355  						Monorail: &pb.MonorailComponent{
   356  							Project: "chromium",
   357  							Value:   "Component>Value",
   358  						},
   359  					},
   360  				},
   361  			}
   362  			testutil.MustApply(ctx, insert.Invocation("inv", pb.Invocation_ACTIVE, nil))
   363  			testutil.MustApply(ctx, insert.TestResultMessages(expected)...)
   364  
   365  			actual, _ := mustFetch(q)
   366  			So(actual, ShouldResembleProto, expected)
   367  		})
   368  
   369  		Convey(`Failure reason`, func() {
   370  			expected := insert.MakeTestResults("inv1", "DoFailureReason", nil, pb.TestStatus_PASS)
   371  			expected[0].FailureReason = &pb.FailureReason{
   372  				PrimaryErrorMessage: "want true, got false",
   373  				Errors: []*pb.FailureReason_Error{
   374  					{Message: "want true, got false"},
   375  					{Message: "want false, got true"},
   376  				},
   377  				TruncatedErrorsCount: 0,
   378  			}
   379  			testutil.MustApply(ctx, insert.Invocation("inv", pb.Invocation_ACTIVE, nil))
   380  			testutil.MustApply(ctx, insert.TestResultMessages(expected)...)
   381  
   382  			actual, _ := mustFetch(q)
   383  			So(actual, ShouldResembleProto, expected)
   384  		})
   385  
   386  		Convey(`Properties`, func() {
   387  			expected := insert.MakeTestResults("inv1", "WithProperties", nil, pb.TestStatus_PASS)
   388  			expected[0].Properties = &structpb.Struct{Fields: map[string]*structpb.Value{
   389  				"key": structpb.NewStringValue("value"),
   390  			}}
   391  			testutil.MustApply(ctx, insert.Invocation("inv", pb.Invocation_ACTIVE, nil))
   392  			testutil.MustApply(ctx, insert.TestResultMessages(expected)...)
   393  
   394  			actual, _ := mustFetch(q)
   395  			So(actual, ShouldResembleProto, expected)
   396  		})
   397  
   398  		Convey(`Skip reason`, func() {
   399  			expected := insert.MakeTestResults("inv1", "WithSkipReason", nil, pb.TestStatus_SKIP)
   400  			expected[0].SkipReason = pb.SkipReason_AUTOMATICALLY_DISABLED_FOR_FLAKINESS
   401  			testutil.MustApply(ctx, insert.Invocation("inv", pb.Invocation_ACTIVE, nil))
   402  			testutil.MustApply(ctx, insert.TestResultMessages(expected)...)
   403  
   404  			actual, _ := mustFetch(q)
   405  			So(actual, ShouldResembleProto, expected)
   406  		})
   407  
   408  		Convey(`Variant in the mask`, func() {
   409  			testutil.MustApply(ctx, insert.Invocation("inv0", pb.Invocation_ACTIVE, nil))
   410  
   411  			v1 := pbutil.Variant("k", "1")
   412  			v2 := pbutil.Variant("k", "2")
   413  			testutil.MustApply(ctx, testutil.CombineMutations(
   414  				insert.TestResults("inv0", "1-1", v1, pb.TestStatus_PASS, pb.TestStatus_FAIL),
   415  				insert.TestResults("inv0", "1-2", v2, pb.TestStatus_PASS),
   416  				insert.TestResults("inv1", "1-1", v1, pb.TestStatus_PASS),
   417  				insert.TestResults("inv1", "2-1", v2, pb.TestStatus_PASS),
   418  			)...)
   419  
   420  			q.InvocationIDs = invocations.NewIDSet("inv0", "inv1")
   421  
   422  			Convey(`Present`, func() {
   423  				q.Mask = mask.MustFromReadMask(&pb.TestResult{}, "name", "test_id", "variant", "variant_hash")
   424  				results, _ := mustFetch(q)
   425  				for _, r := range results {
   426  					So(r.Variant, ShouldNotBeNil)
   427  					So(r.VariantHash, ShouldNotEqual, "")
   428  				}
   429  			})
   430  
   431  			Convey(`Not present`, func() {
   432  				q.Mask = mask.MustFromReadMask(&pb.TestResult{}, "name", "test_id")
   433  				results, _ := mustFetch(q)
   434  				for _, r := range results {
   435  					So(r.Variant, ShouldBeNil)
   436  					So(r.VariantHash, ShouldEqual, "")
   437  				}
   438  			})
   439  		})
   440  
   441  		Convey(`Empty list of invocations`, func() {
   442  			q.InvocationIDs = invocations.NewIDSet()
   443  			actual, _ := mustFetch(q)
   444  			So(actual, ShouldHaveLength, 0)
   445  		})
   446  
   447  		Convey(`Filter invocations`, func() {
   448  			testutil.MustApply(ctx,
   449  				insert.Invocation("inv0", pb.Invocation_ACTIVE, map[string]any{
   450  					"CommonTestIDPrefix": "ninja://browser_tests/",
   451  				}),
   452  				insert.Invocation("inv2", pb.Invocation_ACTIVE, nil),
   453  			)
   454  
   455  			v1 := pbutil.Variant("k", "1")
   456  			v2 := pbutil.Variant("k", "2")
   457  			testutil.MustApply(ctx, testutil.CombineMutations(
   458  				insert.TestResults("inv0", "ninja://browser_tests/1-1", v1, pb.TestStatus_PASS, pb.TestStatus_FAIL),
   459  				insert.TestResults("inv0", "1-2", v2, pb.TestStatus_PASS),
   460  				insert.TestResults("inv1", "ninja://browser_tests/1-1", v1, pb.TestStatus_PASS),
   461  				insert.TestResults("inv1", "2-1", v2, pb.TestStatus_PASS),
   462  				insert.TestResults("inv2", "ninja://browser_tests/1-1", v1, pb.TestStatus_PASS),
   463  			)...)
   464  
   465  			q.InvocationIDs = invocations.NewIDSet("inv0", "inv1")
   466  			q.Predicate.TestIdRegexp = "ninja://browser_tests/.*"
   467  
   468  			results, _ := mustFetch(q)
   469  			for _, r := range results {
   470  				So(r.Name, ShouldNotStartWith, "invocations/inv2")
   471  				So(r.TestId, ShouldEqual, "ninja://browser_tests/1-1")
   472  			}
   473  		})
   474  
   475  		Convey(`Query statements`, func() {
   476  			Convey(`only unexpected exclude exonerated`, func() {
   477  				st := q.genStatement("testResults", map[string]any{
   478  					"params": map[string]any{
   479  						"invIDs":            q.InvocationIDs,
   480  						"afterInvocationId": "build-123",
   481  						"afterTestId":       "test",
   482  						"afterResultId":     "result",
   483  					},
   484  					"columns":           "InvocationId, TestId, VariantHash",
   485  					"onlyUnexpected":    true,
   486  					"withUnexpected":    true,
   487  					"excludeExonerated": true,
   488  				})
   489  				expected := `
   490    				@{USE_ADDITIONAL_PARALLELISM=TRUE}
   491    				WITH
   492  						invs AS (
   493  							SELECT *
   494  							FROM UNNEST(@invIDs)
   495  							AS InvocationId
   496  						),
   497  						testVariants AS (
   498  							SELECT DISTINCT TestId, VariantHash
   499  							FROM invs
   500  							JOIN TestResults@{FORCE_INDEX=UnexpectedTestResults, spanner_emulator.disable_query_null_filtered_index_check=true} USING(InvocationId)
   501  							WHERE IsUnexpected
   502  						),
   503  						exonerated AS (
   504  							SELECT DISTINCT TestId, VariantHash
   505  							FROM TestExonerations
   506  							WHERE InvocationId IN UNNEST(@invIDs)
   507  						),
   508  						variantsWithUnexpectedResults AS (
   509  							SELECT
   510  								tv.*
   511  							FROM testVariants tv
   512  							LEFT JOIN exonerated USING(TestId, VariantHash)
   513  							WHERE exonerated.TestId IS NULL
   514  						),
   515    					withUnexpected AS (
   516    						SELECT InvocationId, TestId, VariantHash
   517  							FROM invs
   518  							JOIN (
   519  								variantsWithUnexpectedResults vur
   520  								JOIN@{FORCE_JOIN_ORDER=TRUE, JOIN_METHOD=HASH_JOIN} TestResults tr USING (TestId, VariantHash)
   521  							) USING(InvocationId)
   522  						) ,
   523  						withOnlyUnexpected AS (
   524  							SELECT ARRAY_AGG(tr) trs
   525  							FROM withUnexpected tr
   526  							GROUP BY TestId, VariantHash
   527  							HAVING LOGICAL_AND(IFNULL(IsUnexpected, false))
   528  						)
   529    				SELECT tr.*
   530    				FROM withOnlyUnexpected owu, owu.trs tr
   531  					WHERE true
   532  						AND (
   533  							(InvocationId > @afterInvocationId) OR
   534  							(InvocationId = @afterInvocationId AND TestId > @afterTestId) OR
   535  							(InvocationId = @afterInvocationId AND TestId = @afterTestId AND ResultId > @afterResultId)
   536  						)
   537  					ORDER BY InvocationId, TestId, ResultId
   538  				`
   539  				So(strings.Join(strings.Fields(st.SQL), " "), ShouldEqual, strings.Join(strings.Fields(expected), " "))
   540  			})
   541  
   542  			Convey(`with unexpected filter by testID`, func() {
   543  				st := q.genStatement("testResults", map[string]any{
   544  					"params": map[string]any{
   545  						"invIDs":            q.InvocationIDs,
   546  						"testIdRegexp":      "^T4$",
   547  						"afterInvocationId": "build-123",
   548  						"afterTestId":       "test",
   549  						"afterResultId":     "result",
   550  					},
   551  					"columns":           "InvocationId, TestId, VariantHash",
   552  					"onlyUnexpected":    false,
   553  					"withUnexpected":    true,
   554  					"excludeExonerated": false,
   555  				})
   556  				expected := `
   557  					@{USE_ADDITIONAL_PARALLELISM=TRUE}
   558  					WITH
   559  						invs AS (
   560  							SELECT *
   561  							FROM UNNEST(@invIDs)
   562  							AS InvocationId
   563  						),
   564  						variantsWithUnexpectedResults AS (
   565  							SELECT DISTINCT TestId, VariantHash
   566  							FROM invs
   567  							JOIN TestResults@{FORCE_INDEX=UnexpectedTestResults, spanner_emulator.disable_query_null_filtered_index_check=true} USING(InvocationId)
   568  							WHERE IsUnexpected
   569  							AND REGEXP_CONTAINS(TestId, @testIdRegexp)
   570  						),
   571  						withUnexpected AS (
   572  							SELECT InvocationId, TestId, VariantHash
   573  							FROM invs
   574  							JOIN (
   575  								variantsWithUnexpectedResults vur
   576  								JOIN@{FORCE_JOIN_ORDER=TRUE, JOIN_METHOD=HASH_JOIN} TestResults tr USING (TestId, VariantHash)
   577  							) USING(InvocationId)
   578  						)
   579  					SELECT * FROM withUnexpected
   580  					WHERE true
   581  						AND (
   582  							(InvocationId > @afterInvocationId) OR
   583  							(InvocationId = @afterInvocationId AND TestId > @afterTestId) OR
   584  							(InvocationId = @afterInvocationId AND TestId = @afterTestId AND ResultId > @afterResultId)
   585  						)
   586  					ORDER BY InvocationId, TestId, ResultId
   587  					`
   588  				// Compare sql strings ignoring whitespaces.
   589  				So(strings.Join(strings.Fields(st.SQL), " "), ShouldEqual, strings.Join(strings.Fields(expected), " "))
   590  			})
   591  		})
   592  	})
   593  }
   594  
   595  func TestToLimitedData(t *testing.T) {
   596  	ctx := context.Background()
   597  
   598  	Convey(`ToLimitedData`, t, func() {
   599  		invID := "inv0"
   600  		testID := "FooBar"
   601  		resultID := "123"
   602  		name := pbutil.TestResultName(invID, testID, resultID)
   603  		variant := pbutil.Variant()
   604  		variantHash := pbutil.VariantHash(variant)
   605  
   606  		Convey(`masks fields`, func() {
   607  			testResult := &pb.TestResult{
   608  				Name:        name,
   609  				TestId:      testID,
   610  				ResultId:    resultID,
   611  				Variant:     variant,
   612  				Expected:    true,
   613  				Status:      pb.TestStatus_PASS,
   614  				SummaryHtml: "SummaryHtml",
   615  				StartTime:   &timestamppb.Timestamp{Seconds: 1000, Nanos: 1234},
   616  				Duration:    &durpb.Duration{Seconds: int64(123), Nanos: 234567000},
   617  				Tags:        pbutil.StringPairs("k1", "v1", "k2", "v2"),
   618  				VariantHash: variantHash,
   619  				TestMetadata: &pb.TestMetadata{
   620  					Name: "name",
   621  					Location: &pb.TestLocation{
   622  						Repo:     "https://chromium.googlesource.com/chromium/src",
   623  						FileName: "//artifact_dir/a_test.cc",
   624  						Line:     54,
   625  					},
   626  					BugComponent: &pb.BugComponent{
   627  						System: &pb.BugComponent_Monorail{
   628  							Monorail: &pb.MonorailComponent{
   629  								Project: "chromium",
   630  								Value:   "Component>Value",
   631  							},
   632  						},
   633  					},
   634  				},
   635  				FailureReason: &pb.FailureReason{
   636  					PrimaryErrorMessage: "an error message",
   637  					Errors: []*pb.FailureReason_Error{
   638  						{Message: "an error message"},
   639  						{Message: "an error message2"},
   640  					},
   641  					TruncatedErrorsCount: 0,
   642  				},
   643  				Properties: &structpb.Struct{
   644  					Fields: map[string]*structpb.Value{
   645  						"key": structpb.NewStringValue("value"),
   646  					},
   647  				},
   648  				SkipReason: pb.SkipReason_SKIP_REASON_UNSPECIFIED,
   649  			}
   650  			So(pbutil.ValidateTestResult(testclock.TestRecentTimeUTC, testResult), ShouldBeNil)
   651  
   652  			expected := &pb.TestResult{
   653  				Name:        name,
   654  				TestId:      testID,
   655  				ResultId:    resultID,
   656  				Expected:    true,
   657  				Status:      pb.TestStatus_PASS,
   658  				StartTime:   &timestamppb.Timestamp{Seconds: 1000, Nanos: 1234},
   659  				Duration:    &durpb.Duration{Seconds: int64(123), Nanos: 234567000},
   660  				VariantHash: variantHash,
   661  				FailureReason: &pb.FailureReason{
   662  					PrimaryErrorMessage: "an error message",
   663  					Errors: []*pb.FailureReason_Error{
   664  						{Message: "an error message"},
   665  						{Message: "an error message2"},
   666  					},
   667  					TruncatedErrorsCount: 0,
   668  				},
   669  				IsMasked:   true,
   670  				SkipReason: pb.SkipReason_SKIP_REASON_UNSPECIFIED,
   671  			}
   672  			So(pbutil.ValidateTestResult(testclock.TestRecentTimeUTC, expected), ShouldBeNil)
   673  
   674  			err := ToLimitedData(ctx, testResult)
   675  			So(err, ShouldBeNil)
   676  			So(testResult, ShouldResembleProto, expected)
   677  		})
   678  
   679  		Convey(`truncates primary error message`, func() {
   680  			testResult := &pb.TestResult{
   681  				Name:        name,
   682  				TestId:      testID,
   683  				ResultId:    resultID,
   684  				Variant:     variant,
   685  				Expected:    false,
   686  				Status:      pb.TestStatus_FAIL,
   687  				SummaryHtml: "SummaryHtml",
   688  				StartTime:   &timestamppb.Timestamp{Seconds: 1000, Nanos: 1234},
   689  				Duration:    &durpb.Duration{Seconds: int64(123), Nanos: 234567000},
   690  				Tags:        pbutil.StringPairs("k1", "v1", "k2", "v2"),
   691  				VariantHash: variantHash,
   692  				TestMetadata: &pb.TestMetadata{
   693  					Name: "name",
   694  					Location: &pb.TestLocation{
   695  						Repo:     "https://chromium.googlesource.com/chromium/src",
   696  						FileName: "//artifact_dir/a_test.cc",
   697  						Line:     54,
   698  					},
   699  					BugComponent: &pb.BugComponent{
   700  						System: &pb.BugComponent_Monorail{
   701  							Monorail: &pb.MonorailComponent{
   702  								Project: "chromium",
   703  								Value:   "Component>Value",
   704  							},
   705  						},
   706  					},
   707  				},
   708  				FailureReason: &pb.FailureReason{
   709  					PrimaryErrorMessage: strings.Repeat("a very long error message", 10),
   710  					Errors: []*pb.FailureReason_Error{
   711  						{
   712  							Message: strings.Repeat("a very long error message",
   713  								10),
   714  						},
   715  						{
   716  							Message: strings.Repeat("a very long error message2",
   717  								10),
   718  						},
   719  					},
   720  					TruncatedErrorsCount: 0,
   721  				},
   722  				Properties: &structpb.Struct{
   723  					Fields: map[string]*structpb.Value{
   724  						"key": structpb.NewStringValue("value"),
   725  					},
   726  				},
   727  				SkipReason: pb.SkipReason_SKIP_REASON_UNSPECIFIED,
   728  			}
   729  			So(pbutil.ValidateTestResult(testclock.TestRecentTimeUTC, testResult), ShouldBeNil)
   730  
   731  			limitedLongErrMsg := strings.Repeat("a very long error message",
   732  				10)[:limitedReasonLength] + "..."
   733  			limitedLongErrMsg2 := strings.Repeat("a very long error message2",
   734  				10)[:limitedReasonLength] + "..."
   735  			expected := &pb.TestResult{
   736  				Name:        name,
   737  				TestId:      testID,
   738  				ResultId:    resultID,
   739  				Expected:    false,
   740  				Status:      pb.TestStatus_FAIL,
   741  				StartTime:   &timestamppb.Timestamp{Seconds: 1000, Nanos: 1234},
   742  				Duration:    &durpb.Duration{Seconds: int64(123), Nanos: 234567000},
   743  				VariantHash: variantHash,
   744  				FailureReason: &pb.FailureReason{
   745  					PrimaryErrorMessage: limitedLongErrMsg,
   746  					Errors: []*pb.FailureReason_Error{
   747  						{Message: limitedLongErrMsg},
   748  						{Message: limitedLongErrMsg2},
   749  					},
   750  					TruncatedErrorsCount: 0,
   751  				},
   752  				IsMasked:   true,
   753  				SkipReason: pb.SkipReason_SKIP_REASON_UNSPECIFIED,
   754  			}
   755  			So(pbutil.ValidateTestResult(testclock.TestRecentTimeUTC, expected), ShouldBeNil)
   756  
   757  			err := ToLimitedData(ctx, testResult)
   758  			So(err, ShouldBeNil)
   759  			So(testResult, ShouldResembleProto, expected)
   760  		})
   761  		Convey(`mask preserves skip reason`, func() {
   762  			testResult := &pb.TestResult{
   763  				Name:        name,
   764  				TestId:      testID,
   765  				ResultId:    resultID,
   766  				Variant:     variant,
   767  				Expected:    true,
   768  				Status:      pb.TestStatus_SKIP,
   769  				VariantHash: variantHash,
   770  				SkipReason:  pb.SkipReason_AUTOMATICALLY_DISABLED_FOR_FLAKINESS,
   771  			}
   772  			So(pbutil.ValidateTestResult(testclock.TestRecentTimeUTC, testResult), ShouldBeNil)
   773  
   774  			expected := &pb.TestResult{
   775  				Name:        name,
   776  				TestId:      testID,
   777  				ResultId:    resultID,
   778  				Expected:    true,
   779  				Status:      pb.TestStatus_SKIP,
   780  				VariantHash: variantHash,
   781  				IsMasked:    true,
   782  				SkipReason:  pb.SkipReason_AUTOMATICALLY_DISABLED_FOR_FLAKINESS,
   783  			}
   784  			So(pbutil.ValidateTestResult(testclock.TestRecentTimeUTC, expected), ShouldBeNil)
   785  
   786  			err := ToLimitedData(ctx, testResult)
   787  			So(err, ShouldBeNil)
   788  			So(testResult, ShouldResembleProto, expected)
   789  		})
   790  	})
   791  }