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 }