go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/artifacts/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 artifacts
    16  
    17  import (
    18  	"testing"
    19  
    20  	"google.golang.org/grpc/codes"
    21  
    22  	"go.chromium.org/luci/server/span"
    23  
    24  	"go.chromium.org/luci/resultdb/internal/invocations"
    25  	"go.chromium.org/luci/resultdb/internal/testutil"
    26  	"go.chromium.org/luci/resultdb/internal/testutil/insert"
    27  	"go.chromium.org/luci/resultdb/pbutil"
    28  	pb "go.chromium.org/luci/resultdb/proto/v1"
    29  
    30  	. "github.com/smartystreets/goconvey/convey"
    31  	. "go.chromium.org/luci/common/testing/assertions"
    32  )
    33  
    34  func TestQuery(t *testing.T) {
    35  	Convey(`Query`, t, func() {
    36  		ctx := testutil.SpannerTestContext(t)
    37  
    38  		testutil.MustApply(ctx, insert.Invocation("inv1", pb.Invocation_ACTIVE, nil))
    39  		q := &Query{
    40  			InvocationIDs:       invocations.NewIDSet("inv1"),
    41  			PageSize:            100,
    42  			TestResultPredicate: &pb.TestResultPredicate{},
    43  			WithRBECASHash:      false,
    44  		}
    45  
    46  		mustFetch := func(q *Query) (arts []*pb.Artifact, token string) {
    47  			ctx, cancel := span.ReadOnlyTransaction(ctx)
    48  			defer cancel()
    49  			arts, tok, err := q.FetchProtos(ctx)
    50  			So(err, ShouldBeNil)
    51  			return arts, tok
    52  		}
    53  
    54  		mustFetchNames := func(q *Query) []string {
    55  			arts, _ := mustFetch(q)
    56  			names := make([]string, len(arts))
    57  			for i, a := range arts {
    58  				names[i] = a.Name
    59  			}
    60  			return names
    61  		}
    62  
    63  		Convey(`Populates fields correctly`, func() {
    64  			testutil.MustApply(ctx,
    65  				insert.Artifact("inv1", "", "a", map[string]any{
    66  					"ContentType": "text/plain",
    67  					"Size":        64,
    68  				}),
    69  			)
    70  			actual, _ := mustFetch(q)
    71  			So(actual, ShouldHaveLength, 1)
    72  			So(actual[0].ContentType, ShouldEqual, "text/plain")
    73  			So(actual[0].SizeBytes, ShouldEqual, 64)
    74  		})
    75  
    76  		Convey(`Reads both invocation and test result artifacts`, func() {
    77  			testutil.MustApply(ctx,
    78  				insert.Artifact("inv1", "", "a", nil),
    79  				insert.Artifact("inv1", "tr/t t/r", "a", nil),
    80  			)
    81  			actual := mustFetchNames(q)
    82  			So(actual, ShouldResemble, []string{
    83  				"invocations/inv1/artifacts/a",
    84  				"invocations/inv1/tests/t%20t/results/r/artifacts/a",
    85  			})
    86  		})
    87  
    88  		Convey(`Does not fetch artifacts of other invocations`, func() {
    89  			testutil.MustApply(ctx,
    90  				insert.Invocation("inv0", pb.Invocation_ACTIVE, nil),
    91  				insert.Invocation("inv2", pb.Invocation_ACTIVE, nil),
    92  				insert.Artifact("inv0", "", "a", nil),
    93  				insert.Artifact("inv1", "", "a", nil),
    94  				insert.Artifact("inv2", "", "a", nil),
    95  			)
    96  			actual := mustFetchNames(q)
    97  			So(actual, ShouldResemble, []string{"invocations/inv1/artifacts/a"})
    98  		})
    99  
   100  		Convey(`Test ID regexp`, func() {
   101  			testutil.MustApply(ctx,
   102  				insert.Artifact("inv1", "", "a", nil),
   103  				insert.Artifact("inv1", "tr/t00/r", "a", nil),
   104  				insert.Artifact("inv1", "tr/t10/r", "a", nil),
   105  				insert.Artifact("inv1", "tr/t11/r", "a", nil),
   106  				insert.Artifact("inv1", "tr/t20/r", "a", nil),
   107  			)
   108  			q.TestResultPredicate.TestIdRegexp = "t1."
   109  			actual := mustFetchNames(q)
   110  			So(actual, ShouldResemble, []string{
   111  				"invocations/inv1/artifacts/a",
   112  				"invocations/inv1/tests/t10/results/r/artifacts/a",
   113  				"invocations/inv1/tests/t11/results/r/artifacts/a",
   114  			})
   115  		})
   116  
   117  		Convey(`Follow edges`, func() {
   118  			testutil.MustApply(ctx,
   119  				insert.Artifact("inv1", "", "a0", nil),
   120  				insert.Artifact("inv1", "", "a1", nil),
   121  				insert.Artifact("inv1", "tr/t/r", "a0", nil),
   122  				insert.Artifact("inv1", "tr/t/r", "a1", nil),
   123  			)
   124  
   125  			Convey(`Unspecified`, func() {
   126  				actual := mustFetchNames(q)
   127  				So(actual, ShouldResemble, []string{
   128  					"invocations/inv1/artifacts/a0",
   129  					"invocations/inv1/artifacts/a1",
   130  					"invocations/inv1/tests/t/results/r/artifacts/a0",
   131  					"invocations/inv1/tests/t/results/r/artifacts/a1",
   132  				})
   133  			})
   134  
   135  			Convey(`Only invocations`, func() {
   136  				q.FollowEdges = &pb.ArtifactPredicate_EdgeTypeSet{
   137  					IncludedInvocations: true,
   138  				}
   139  				actual := mustFetchNames(q)
   140  				So(actual, ShouldResemble, []string{
   141  					"invocations/inv1/artifacts/a0",
   142  					"invocations/inv1/artifacts/a1",
   143  				})
   144  
   145  				Convey(`Test result predicate is ignored`, func() {
   146  					q.TestResultPredicate.TestIdRegexp = "t."
   147  					actual := mustFetchNames(q)
   148  					So(actual, ShouldResemble, []string{
   149  						"invocations/inv1/artifacts/a0",
   150  						"invocations/inv1/artifacts/a1",
   151  					})
   152  				})
   153  			})
   154  
   155  			Convey(`Only test results`, func() {
   156  				q.FollowEdges = &pb.ArtifactPredicate_EdgeTypeSet{
   157  					TestResults: true,
   158  				}
   159  				actual := mustFetchNames(q)
   160  				So(actual, ShouldResemble, []string{
   161  					"invocations/inv1/tests/t/results/r/artifacts/a0",
   162  					"invocations/inv1/tests/t/results/r/artifacts/a1",
   163  				})
   164  			})
   165  
   166  			Convey(`Both`, func() {
   167  				q.FollowEdges = &pb.ArtifactPredicate_EdgeTypeSet{
   168  					IncludedInvocations: true,
   169  					TestResults:         true,
   170  				}
   171  				actual := mustFetchNames(q)
   172  				So(actual, ShouldResemble, []string{
   173  					"invocations/inv1/artifacts/a0",
   174  					"invocations/inv1/artifacts/a1",
   175  					"invocations/inv1/tests/t/results/r/artifacts/a0",
   176  					"invocations/inv1/tests/t/results/r/artifacts/a1",
   177  				})
   178  			})
   179  		})
   180  
   181  		Convey(`Artifacts of interesting test results`, func() {
   182  			testutil.MustApply(ctx,
   183  				insert.Artifact("inv1", "", "a", nil),
   184  				insert.Artifact("inv1", "tr/t0/0", "a", nil),
   185  				insert.Artifact("inv1", "tr/t1/0", "a", nil),
   186  				insert.Artifact("inv1", "tr/t1/1", "a", nil),
   187  				insert.Artifact("inv1", "tr/t1/1", "b", nil),
   188  				insert.Artifact("inv1", "tr/t2/0", "a", nil),
   189  			)
   190  			testutil.MustApply(ctx, testutil.CombineMutations(
   191  				insert.TestResults("inv1", "t0", nil, pb.TestStatus_PASS),
   192  				insert.TestResults("inv1", "t1", nil, pb.TestStatus_PASS, pb.TestStatus_FAIL),
   193  				insert.TestResults("inv1", "t2", nil, pb.TestStatus_FAIL),
   194  			)...)
   195  
   196  			q.TestResultPredicate.Expectancy = pb.TestResultPredicate_VARIANTS_WITH_UNEXPECTED_RESULTS
   197  			actual := mustFetchNames(q)
   198  			So(actual, ShouldResemble, []string{
   199  				"invocations/inv1/artifacts/a",
   200  				"invocations/inv1/tests/t1/results/0/artifacts/a",
   201  				"invocations/inv1/tests/t1/results/1/artifacts/a",
   202  				"invocations/inv1/tests/t1/results/1/artifacts/b",
   203  				"invocations/inv1/tests/t2/results/0/artifacts/a",
   204  			})
   205  
   206  			Convey(`Without invocation artifacts`, func() {
   207  				q.FollowEdges = &pb.ArtifactPredicate_EdgeTypeSet{TestResults: true}
   208  				actual := mustFetchNames(q)
   209  				So(actual, ShouldNotContain, "invocations/inv1/artifacts/a")
   210  			})
   211  		})
   212  
   213  		Convey(`Artifacts of unexpected test results`, func() {
   214  			testutil.MustApply(ctx,
   215  				insert.Artifact("inv1", "", "a", nil),
   216  				insert.Artifact("inv1", "tr/t0/0", "a", nil),
   217  				insert.Artifact("inv1", "tr/t1/0", "a", nil),
   218  				insert.Artifact("inv1", "tr/t1/1", "a", nil),
   219  				insert.Artifact("inv1", "tr/t1/1", "b", nil),
   220  				insert.Artifact("inv1", "tr/t2/0", "a", nil),
   221  			)
   222  			testutil.MustApply(ctx, testutil.CombineMutations(
   223  				insert.TestResults("inv1", "t0", nil, pb.TestStatus_PASS),
   224  				insert.TestResults("inv1", "t1", nil, pb.TestStatus_PASS, pb.TestStatus_FAIL),
   225  				insert.TestResults("inv1", "t2", nil, pb.TestStatus_FAIL),
   226  			)...)
   227  
   228  			q.TestResultPredicate.Expectancy = pb.TestResultPredicate_VARIANTS_WITH_ONLY_UNEXPECTED_RESULTS
   229  			actual := mustFetchNames(q)
   230  			So(actual, ShouldResemble, []string{
   231  				"invocations/inv1/artifacts/a",
   232  				"invocations/inv1/tests/t2/results/0/artifacts/a",
   233  			})
   234  
   235  		})
   236  
   237  		Convey(`Variant equals`, func() {
   238  			testutil.MustApply(ctx,
   239  				insert.Artifact("inv1", "", "a", nil),
   240  				insert.Artifact("inv1", "tr/t0/0", "a", nil),
   241  				insert.Artifact("inv1", "tr/t1/0", "a", nil),
   242  				insert.Artifact("inv1", "tr/t1/0", "b", nil),
   243  				insert.Artifact("inv1", "tr/t2/0", "a", nil),
   244  			)
   245  			v1 := pbutil.Variant("k", "1")
   246  			v2 := pbutil.Variant("k", "2")
   247  			testutil.MustApply(ctx, testutil.CombineMutations(
   248  				insert.TestResults("inv1", "t0", v1, pb.TestStatus_PASS),
   249  				insert.TestResults("inv1", "t1", v2, pb.TestStatus_PASS),
   250  				insert.TestResults("inv1", "t2", v1, pb.TestStatus_PASS),
   251  			)...)
   252  
   253  			q.TestResultPredicate.Variant = &pb.VariantPredicate{
   254  				Predicate: &pb.VariantPredicate_Equals{Equals: v2},
   255  			}
   256  			So(mustFetchNames(q), ShouldResemble, []string{
   257  				"invocations/inv1/artifacts/a",
   258  				"invocations/inv1/tests/t1/results/0/artifacts/a",
   259  				"invocations/inv1/tests/t1/results/0/artifacts/b",
   260  			})
   261  
   262  			Convey(`Without invocation artifacts`, func() {
   263  				q.FollowEdges = &pb.ArtifactPredicate_EdgeTypeSet{TestResults: true}
   264  				actual := mustFetchNames(q)
   265  				So(actual, ShouldNotContain, "invocations/inv1/artifacts/a")
   266  			})
   267  		})
   268  
   269  		Convey(`Variant contains`, func() {
   270  			testutil.MustApply(ctx,
   271  				insert.Artifact("inv1", "", "a", nil),
   272  				insert.Artifact("inv1", "tr/t0/0", "a", nil),
   273  				insert.Artifact("inv1", "tr/t1/0", "a", nil),
   274  				insert.Artifact("inv1", "tr/t1/0", "b", nil),
   275  				insert.Artifact("inv1", "tr/t2/0", "a", nil),
   276  			)
   277  			v00 := pbutil.Variant("k0", "0")
   278  			v01 := pbutil.Variant("k0", "0", "k1", "1")
   279  			v10 := pbutil.Variant("k0", "1")
   280  			testutil.MustApply(ctx, testutil.CombineMutations(
   281  				insert.TestResults("inv1", "t0", v00, pb.TestStatus_PASS),
   282  				insert.TestResults("inv1", "t1", v01, pb.TestStatus_PASS),
   283  				insert.TestResults("inv1", "t2", v10, pb.TestStatus_PASS),
   284  			)...)
   285  
   286  			Convey(`Empty`, func() {
   287  				q.TestResultPredicate.Variant = &pb.VariantPredicate{
   288  					Predicate: &pb.VariantPredicate_Contains{Contains: pbutil.Variant()},
   289  				}
   290  				So(mustFetchNames(q), ShouldResemble, []string{
   291  					"invocations/inv1/artifacts/a",
   292  					"invocations/inv1/tests/t0/results/0/artifacts/a",
   293  					"invocations/inv1/tests/t1/results/0/artifacts/a",
   294  					"invocations/inv1/tests/t1/results/0/artifacts/b",
   295  					"invocations/inv1/tests/t2/results/0/artifacts/a",
   296  				})
   297  
   298  				Convey(`Without invocation artifacts`, func() {
   299  					q.FollowEdges = &pb.ArtifactPredicate_EdgeTypeSet{TestResults: true}
   300  					actual := mustFetchNames(q)
   301  					So(actual, ShouldNotContain, "invocations/inv1/artifacts/a")
   302  				})
   303  			})
   304  
   305  			Convey(`Non-empty`, func() {
   306  				q.TestResultPredicate.Variant = &pb.VariantPredicate{
   307  					Predicate: &pb.VariantPredicate_Contains{Contains: v00},
   308  				}
   309  				So(mustFetchNames(q), ShouldResemble, []string{
   310  					"invocations/inv1/artifacts/a",
   311  					"invocations/inv1/tests/t0/results/0/artifacts/a",
   312  					"invocations/inv1/tests/t1/results/0/artifacts/a",
   313  					"invocations/inv1/tests/t1/results/0/artifacts/b",
   314  				})
   315  
   316  				Convey(`Without invocation artifacts`, func() {
   317  					q.FollowEdges = &pb.ArtifactPredicate_EdgeTypeSet{TestResults: true}
   318  					actual := mustFetchNames(q)
   319  					So(actual, ShouldNotContain, "invocations/inv1/artifacts/a")
   320  				})
   321  			})
   322  		})
   323  
   324  		Convey(`Paging`, func() {
   325  			testutil.MustApply(ctx,
   326  				insert.Artifact("inv1", "", "a0", nil),
   327  				insert.Artifact("inv1", "", "a1", nil),
   328  				insert.Artifact("inv1", "", "a2", nil),
   329  				insert.Artifact("inv1", "", "a3", nil),
   330  				insert.Artifact("inv1", "", "a4", nil),
   331  			)
   332  
   333  			mustReadPage := func(pageToken string, pageSize int, expectedArtifactIDs ...string) string {
   334  				q2 := *q
   335  				q2.PageToken = pageToken
   336  				q2.PageSize = pageSize
   337  				arts, token := mustFetch(&q2)
   338  
   339  				actualArtifactIDs := make([]string, len(arts))
   340  				for i, a := range arts {
   341  					actualArtifactIDs[i] = a.ArtifactId
   342  				}
   343  				So(actualArtifactIDs, ShouldResemble, expectedArtifactIDs)
   344  				return token
   345  			}
   346  
   347  			Convey(`All results`, func() {
   348  				token := mustReadPage("", 10, "a0", "a1", "a2", "a3", "a4")
   349  				So(token, ShouldEqual, "")
   350  			})
   351  
   352  			Convey(`With pagination`, func() {
   353  				token := mustReadPage("", 1, "a0")
   354  				So(token, ShouldNotEqual, "")
   355  
   356  				token = mustReadPage(token, 2, "a1", "a2")
   357  				So(token, ShouldNotEqual, "")
   358  
   359  				token = mustReadPage(token, 3, "a3", "a4")
   360  				So(token, ShouldEqual, "")
   361  			})
   362  
   363  			Convey(`Bad token`, func() {
   364  				q.PageToken = "CgVoZWxsbw=="
   365  				_, _, err := q.FetchProtos(span.Single(ctx))
   366  				So(err, ShouldHaveAppStatus, codes.InvalidArgument, "invalid page_token")
   367  			})
   368  		})
   369  
   370  		Convey(`ContentTypes`, func() {
   371  			Convey(`Works`, func() {
   372  				testutil.MustApply(ctx,
   373  					insert.Artifact("inv1", "", "a0", map[string]any{"ContentType": "text/plain; encoding=utf-8"}),
   374  					insert.Artifact("inv1", "tr/t/r", "a0", map[string]any{"ContentType": "text/plain"}),
   375  					insert.Artifact("inv1", "tr/t/r", "a1", nil),
   376  					insert.Artifact("inv1", "tr/t/r", "a3", map[string]any{"ContentType": "image/jpg"}),
   377  				)
   378  				q.ContentTypeRegexp = "text/.+"
   379  
   380  				actual := mustFetchNames(q)
   381  				So(actual, ShouldResemble, []string{
   382  					"invocations/inv1/artifacts/a0",
   383  					"invocations/inv1/tests/t/results/r/artifacts/a0",
   384  				})
   385  			})
   386  
   387  			Convey(`Filter generated conditionally`, func() {
   388  				q.ContentTypeRegexp = ""
   389  				st, err := q.genStmt(ctx)
   390  				So(err, ShouldBeNil)
   391  				So(st.SQL, ShouldNotContainSubstring, "@contentTypeRegexp")
   392  			})
   393  		})
   394  
   395  		Convey(`ArtifactIds`, func() {
   396  			Convey(`Works`, func() {
   397  				testutil.MustApply(ctx,
   398  					insert.Artifact("inv1", "", "a0", nil),
   399  					insert.Artifact("inv1", "tr/t/r", "a0", nil),
   400  					insert.Artifact("inv1", "tr/t/r", "a1", nil),
   401  					insert.Artifact("inv1", "tr/t/r", "a3", nil),
   402  				)
   403  				q.ArtifactIDRegexp = "a0"
   404  
   405  				actual := mustFetchNames(q)
   406  				So(actual, ShouldResemble, []string{
   407  					"invocations/inv1/artifacts/a0",
   408  					"invocations/inv1/tests/t/results/r/artifacts/a0",
   409  				})
   410  			})
   411  
   412  			Convey(`Filter generated conditionally`, func() {
   413  				q.ArtifactIDRegexp = ""
   414  				st, err := q.genStmt(ctx)
   415  				So(err, ShouldBeNil)
   416  				So(st.SQL, ShouldNotContainSubstring, "@artifactIdRegexp")
   417  			})
   418  		})
   419  
   420  		Convey(`WithRBECASHash`, func() {
   421  			testutil.MustApply(ctx,
   422  				insert.Artifact("inv1", "tr/t/r", "a", map[string]any{
   423  					"ContentType": "text/plain",
   424  					"Size":        64,
   425  					"RBECASHash":  "deadbeef",
   426  				}),
   427  			)
   428  
   429  			q.WithRBECASHash = true
   430  			q.PageSize = 0
   431  			ctx, cancel := span.ReadOnlyTransaction(ctx)
   432  			defer cancel()
   433  			var actual []*Artifact
   434  			err := q.Run(ctx, func(a *Artifact) error {
   435  				actual = append(actual, a)
   436  				return nil
   437  			})
   438  			So(err, ShouldBeNil)
   439  			So(actual, ShouldHaveLength, 1)
   440  			So(actual[0].RBECASHash, ShouldEqual, "deadbeef")
   441  		})
   442  
   443  		Convey(`WithGcsURI`, func() {
   444  			testutil.MustApply(ctx,
   445  				insert.Artifact("inv1", "tr/t/r", "a", map[string]any{
   446  					"ContentType": "text/plain",
   447  					"Size":        64,
   448  					"GcsURI":      "gs://bucket/beyondbeef",
   449  				}),
   450  			)
   451  
   452  			q.WithGcsURI = true
   453  			q.PageSize = 0
   454  			ctx, cancel := span.ReadOnlyTransaction(ctx)
   455  			defer cancel()
   456  			var actual []*Artifact
   457  			err := q.Run(ctx, func(a *Artifact) error {
   458  				actual = append(actual, a)
   459  				return nil
   460  			})
   461  			So(err, ShouldBeNil)
   462  			So(actual, ShouldHaveLength, 1)
   463  			So(actual[0].GcsUri, ShouldEqual, "gs://bucket/beyondbeef")
   464  		})
   465  	})
   466  }