blob: c457944e9a82e79c35c0f9c83d2d6fdf860c9b72 [file] [log] [blame]
// Copyright 2020 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package artifacts
import (
"testing"
"google.golang.org/grpc/codes"
"go.chromium.org/luci/server/span"
"go.chromium.org/luci/resultdb/internal/invocations"
"go.chromium.org/luci/resultdb/internal/testutil"
"go.chromium.org/luci/resultdb/internal/testutil/insert"
"go.chromium.org/luci/resultdb/pbutil"
pb "go.chromium.org/luci/resultdb/proto/v1"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
func TestQuery(t *testing.T) {
Convey(`Query`, t, func() {
ctx := testutil.SpannerTestContext(t)
testutil.MustApply(ctx, insert.Invocation("inv1", pb.Invocation_ACTIVE, nil))
q := &Query{
InvocationIDs: invocations.NewIDSet("inv1"),
PageSize: 100,
TestResultPredicate: &pb.TestResultPredicate{},
WithRBECASHash: false,
}
mustFetch := func(q *Query) (arts []*pb.Artifact, token string) {
ctx, cancel := span.ReadOnlyTransaction(ctx)
defer cancel()
arts, tok, err := q.FetchProtos(ctx)
So(err, ShouldBeNil)
return arts, tok
}
mustFetchNames := func(q *Query) []string {
arts, _ := mustFetch(q)
names := make([]string, len(arts))
for i, a := range arts {
names[i] = a.Name
}
return names
}
Convey(`Populates fields correctly`, func() {
testutil.MustApply(ctx,
insert.Artifact("inv1", "", "a", map[string]any{
"ContentType": "text/plain",
"Size": 64,
}),
)
actual, _ := mustFetch(q)
So(actual, ShouldHaveLength, 1)
So(actual[0].ContentType, ShouldEqual, "text/plain")
So(actual[0].SizeBytes, ShouldEqual, 64)
})
Convey(`Reads both invocation and test result artifacts`, func() {
testutil.MustApply(ctx,
insert.Artifact("inv1", "", "a", nil),
insert.Artifact("inv1", "tr/t t/r", "a", nil),
)
actual := mustFetchNames(q)
So(actual, ShouldResemble, []string{
"invocations/inv1/artifacts/a",
"invocations/inv1/tests/t%20t/results/r/artifacts/a",
})
})
Convey(`Does not fetch artifacts of other invocations`, func() {
testutil.MustApply(ctx,
insert.Invocation("inv0", pb.Invocation_ACTIVE, nil),
insert.Invocation("inv2", pb.Invocation_ACTIVE, nil),
insert.Artifact("inv0", "", "a", nil),
insert.Artifact("inv1", "", "a", nil),
insert.Artifact("inv2", "", "a", nil),
)
actual := mustFetchNames(q)
So(actual, ShouldResemble, []string{"invocations/inv1/artifacts/a"})
})
Convey(`Test ID regexp`, func() {
testutil.MustApply(ctx,
insert.Artifact("inv1", "", "a", nil),
insert.Artifact("inv1", "tr/t00/r", "a", nil),
insert.Artifact("inv1", "tr/t10/r", "a", nil),
insert.Artifact("inv1", "tr/t11/r", "a", nil),
insert.Artifact("inv1", "tr/t20/r", "a", nil),
)
q.TestResultPredicate.TestIdRegexp = "t1."
actual := mustFetchNames(q)
So(actual, ShouldResemble, []string{
"invocations/inv1/artifacts/a",
"invocations/inv1/tests/t10/results/r/artifacts/a",
"invocations/inv1/tests/t11/results/r/artifacts/a",
})
})
Convey(`Follow edges`, func() {
testutil.MustApply(ctx,
insert.Artifact("inv1", "", "a0", nil),
insert.Artifact("inv1", "", "a1", nil),
insert.Artifact("inv1", "tr/t/r", "a0", nil),
insert.Artifact("inv1", "tr/t/r", "a1", nil),
)
Convey(`Unspecified`, func() {
actual := mustFetchNames(q)
So(actual, ShouldResemble, []string{
"invocations/inv1/artifacts/a0",
"invocations/inv1/artifacts/a1",
"invocations/inv1/tests/t/results/r/artifacts/a0",
"invocations/inv1/tests/t/results/r/artifacts/a1",
})
})
Convey(`Only invocations`, func() {
q.FollowEdges = &pb.ArtifactPredicate_EdgeTypeSet{
IncludedInvocations: true,
}
actual := mustFetchNames(q)
So(actual, ShouldResemble, []string{
"invocations/inv1/artifacts/a0",
"invocations/inv1/artifacts/a1",
})
Convey(`Test result predicate is ignored`, func() {
q.TestResultPredicate.TestIdRegexp = "t."
actual := mustFetchNames(q)
So(actual, ShouldResemble, []string{
"invocations/inv1/artifacts/a0",
"invocations/inv1/artifacts/a1",
})
})
})
Convey(`Only test results`, func() {
q.FollowEdges = &pb.ArtifactPredicate_EdgeTypeSet{
TestResults: true,
}
actual := mustFetchNames(q)
So(actual, ShouldResemble, []string{
"invocations/inv1/tests/t/results/r/artifacts/a0",
"invocations/inv1/tests/t/results/r/artifacts/a1",
})
})
Convey(`Both`, func() {
q.FollowEdges = &pb.ArtifactPredicate_EdgeTypeSet{
IncludedInvocations: true,
TestResults: true,
}
actual := mustFetchNames(q)
So(actual, ShouldResemble, []string{
"invocations/inv1/artifacts/a0",
"invocations/inv1/artifacts/a1",
"invocations/inv1/tests/t/results/r/artifacts/a0",
"invocations/inv1/tests/t/results/r/artifacts/a1",
})
})
})
Convey(`Artifacts of interesting test results`, func() {
testutil.MustApply(ctx,
insert.Artifact("inv1", "", "a", nil),
insert.Artifact("inv1", "tr/t0/0", "a", nil),
insert.Artifact("inv1", "tr/t1/0", "a", nil),
insert.Artifact("inv1", "tr/t1/1", "a", nil),
insert.Artifact("inv1", "tr/t1/1", "b", nil),
insert.Artifact("inv1", "tr/t2/0", "a", nil),
)
testutil.MustApply(ctx, testutil.CombineMutations(
insert.TestResults("inv1", "t0", nil, pb.TestStatus_PASS),
insert.TestResults("inv1", "t1", nil, pb.TestStatus_PASS, pb.TestStatus_FAIL),
insert.TestResults("inv1", "t2", nil, pb.TestStatus_FAIL),
)...)
q.TestResultPredicate.Expectancy = pb.TestResultPredicate_VARIANTS_WITH_UNEXPECTED_RESULTS
actual := mustFetchNames(q)
So(actual, ShouldResemble, []string{
"invocations/inv1/artifacts/a",
"invocations/inv1/tests/t1/results/0/artifacts/a",
"invocations/inv1/tests/t1/results/1/artifacts/a",
"invocations/inv1/tests/t1/results/1/artifacts/b",
"invocations/inv1/tests/t2/results/0/artifacts/a",
})
Convey(`Without invocation artifacts`, func() {
q.FollowEdges = &pb.ArtifactPredicate_EdgeTypeSet{TestResults: true}
actual := mustFetchNames(q)
So(actual, ShouldNotContain, "invocations/inv1/artifacts/a")
})
})
Convey(`Artifacts of unexpected test results`, func() {
testutil.MustApply(ctx,
insert.Artifact("inv1", "", "a", nil),
insert.Artifact("inv1", "tr/t0/0", "a", nil),
insert.Artifact("inv1", "tr/t1/0", "a", nil),
insert.Artifact("inv1", "tr/t1/1", "a", nil),
insert.Artifact("inv1", "tr/t1/1", "b", nil),
insert.Artifact("inv1", "tr/t2/0", "a", nil),
)
testutil.MustApply(ctx, testutil.CombineMutations(
insert.TestResults("inv1", "t0", nil, pb.TestStatus_PASS),
insert.TestResults("inv1", "t1", nil, pb.TestStatus_PASS, pb.TestStatus_FAIL),
insert.TestResults("inv1", "t2", nil, pb.TestStatus_FAIL),
)...)
q.TestResultPredicate.Expectancy = pb.TestResultPredicate_VARIANTS_WITH_ONLY_UNEXPECTED_RESULTS
actual := mustFetchNames(q)
So(actual, ShouldResemble, []string{
"invocations/inv1/artifacts/a",
"invocations/inv1/tests/t2/results/0/artifacts/a",
})
})
Convey(`Variant equals`, func() {
testutil.MustApply(ctx,
insert.Artifact("inv1", "", "a", nil),
insert.Artifact("inv1", "tr/t0/0", "a", nil),
insert.Artifact("inv1", "tr/t1/0", "a", nil),
insert.Artifact("inv1", "tr/t1/0", "b", nil),
insert.Artifact("inv1", "tr/t2/0", "a", nil),
)
v1 := pbutil.Variant("k", "1")
v2 := pbutil.Variant("k", "2")
testutil.MustApply(ctx, testutil.CombineMutations(
insert.TestResults("inv1", "t0", v1, pb.TestStatus_PASS),
insert.TestResults("inv1", "t1", v2, pb.TestStatus_PASS),
insert.TestResults("inv1", "t2", v1, pb.TestStatus_PASS),
)...)
q.TestResultPredicate.Variant = &pb.VariantPredicate{
Predicate: &pb.VariantPredicate_Equals{Equals: v2},
}
So(mustFetchNames(q), ShouldResemble, []string{
"invocations/inv1/artifacts/a",
"invocations/inv1/tests/t1/results/0/artifacts/a",
"invocations/inv1/tests/t1/results/0/artifacts/b",
})
Convey(`Without invocation artifacts`, func() {
q.FollowEdges = &pb.ArtifactPredicate_EdgeTypeSet{TestResults: true}
actual := mustFetchNames(q)
So(actual, ShouldNotContain, "invocations/inv1/artifacts/a")
})
})
Convey(`Variant contains`, func() {
testutil.MustApply(ctx,
insert.Artifact("inv1", "", "a", nil),
insert.Artifact("inv1", "tr/t0/0", "a", nil),
insert.Artifact("inv1", "tr/t1/0", "a", nil),
insert.Artifact("inv1", "tr/t1/0", "b", nil),
insert.Artifact("inv1", "tr/t2/0", "a", nil),
)
v00 := pbutil.Variant("k0", "0")
v01 := pbutil.Variant("k0", "0", "k1", "1")
v10 := pbutil.Variant("k0", "1")
testutil.MustApply(ctx, testutil.CombineMutations(
insert.TestResults("inv1", "t0", v00, pb.TestStatus_PASS),
insert.TestResults("inv1", "t1", v01, pb.TestStatus_PASS),
insert.TestResults("inv1", "t2", v10, pb.TestStatus_PASS),
)...)
Convey(`Empty`, func() {
q.TestResultPredicate.Variant = &pb.VariantPredicate{
Predicate: &pb.VariantPredicate_Contains{Contains: pbutil.Variant()},
}
So(mustFetchNames(q), ShouldResemble, []string{
"invocations/inv1/artifacts/a",
"invocations/inv1/tests/t0/results/0/artifacts/a",
"invocations/inv1/tests/t1/results/0/artifacts/a",
"invocations/inv1/tests/t1/results/0/artifacts/b",
"invocations/inv1/tests/t2/results/0/artifacts/a",
})
Convey(`Without invocation artifacts`, func() {
q.FollowEdges = &pb.ArtifactPredicate_EdgeTypeSet{TestResults: true}
actual := mustFetchNames(q)
So(actual, ShouldNotContain, "invocations/inv1/artifacts/a")
})
})
Convey(`Non-empty`, func() {
q.TestResultPredicate.Variant = &pb.VariantPredicate{
Predicate: &pb.VariantPredicate_Contains{Contains: v00},
}
So(mustFetchNames(q), ShouldResemble, []string{
"invocations/inv1/artifacts/a",
"invocations/inv1/tests/t0/results/0/artifacts/a",
"invocations/inv1/tests/t1/results/0/artifacts/a",
"invocations/inv1/tests/t1/results/0/artifacts/b",
})
Convey(`Without invocation artifacts`, func() {
q.FollowEdges = &pb.ArtifactPredicate_EdgeTypeSet{TestResults: true}
actual := mustFetchNames(q)
So(actual, ShouldNotContain, "invocations/inv1/artifacts/a")
})
})
})
Convey(`Paging`, func() {
testutil.MustApply(ctx,
insert.Artifact("inv1", "", "a0", nil),
insert.Artifact("inv1", "", "a1", nil),
insert.Artifact("inv1", "", "a2", nil),
insert.Artifact("inv1", "", "a3", nil),
insert.Artifact("inv1", "", "a4", nil),
)
mustReadPage := func(pageToken string, pageSize int, expectedArtifactIDs ...string) string {
q2 := *q
q2.PageToken = pageToken
q2.PageSize = pageSize
arts, token := mustFetch(&q2)
actualArtifactIDs := make([]string, len(arts))
for i, a := range arts {
actualArtifactIDs[i] = a.ArtifactId
}
So(actualArtifactIDs, ShouldResemble, expectedArtifactIDs)
return token
}
Convey(`All results`, func() {
token := mustReadPage("", 10, "a0", "a1", "a2", "a3", "a4")
So(token, ShouldEqual, "")
})
Convey(`With pagination`, func() {
token := mustReadPage("", 1, "a0")
So(token, ShouldNotEqual, "")
token = mustReadPage(token, 2, "a1", "a2")
So(token, ShouldNotEqual, "")
token = mustReadPage(token, 3, "a3", "a4")
So(token, ShouldEqual, "")
})
Convey(`Bad token`, func() {
q.PageToken = "CgVoZWxsbw=="
_, _, err := q.FetchProtos(span.Single(ctx))
So(err, ShouldHaveAppStatus, codes.InvalidArgument, "invalid page_token")
})
})
Convey(`ContentTypes`, func() {
Convey(`Works`, func() {
testutil.MustApply(ctx,
insert.Artifact("inv1", "", "a0", map[string]any{"ContentType": "text/plain; encoding=utf-8"}),
insert.Artifact("inv1", "tr/t/r", "a0", map[string]any{"ContentType": "text/plain"}),
insert.Artifact("inv1", "tr/t/r", "a1", nil),
insert.Artifact("inv1", "tr/t/r", "a3", map[string]any{"ContentType": "image/jpg"}),
)
q.ContentTypeRegexp = "text/.+"
actual := mustFetchNames(q)
So(actual, ShouldResemble, []string{
"invocations/inv1/artifacts/a0",
"invocations/inv1/tests/t/results/r/artifacts/a0",
})
})
Convey(`Filter generated conditionally`, func() {
q.ContentTypeRegexp = ""
st, err := q.genStmt(ctx)
So(err, ShouldBeNil)
So(st.SQL, ShouldNotContainSubstring, "@contentTypeRegexp")
})
})
Convey(`ArtifactIds`, func() {
Convey(`Works`, func() {
testutil.MustApply(ctx,
insert.Artifact("inv1", "", "a0", nil),
insert.Artifact("inv1", "tr/t/r", "a0", nil),
insert.Artifact("inv1", "tr/t/r", "a1", nil),
insert.Artifact("inv1", "tr/t/r", "a3", nil),
)
q.ArtifactIDRegexp = "a0"
actual := mustFetchNames(q)
So(actual, ShouldResemble, []string{
"invocations/inv1/artifacts/a0",
"invocations/inv1/tests/t/results/r/artifacts/a0",
})
})
Convey(`Filter generated conditionally`, func() {
q.ArtifactIDRegexp = ""
st, err := q.genStmt(ctx)
So(err, ShouldBeNil)
So(st.SQL, ShouldNotContainSubstring, "@artifactIdRegexp")
})
})
Convey(`WithRBECASHash`, func() {
testutil.MustApply(ctx,
insert.Artifact("inv1", "tr/t/r", "a", map[string]any{
"ContentType": "text/plain",
"Size": 64,
"RBECASHash": "deadbeef",
}),
)
q.WithRBECASHash = true
q.PageSize = 0
ctx, cancel := span.ReadOnlyTransaction(ctx)
defer cancel()
var actual []*Artifact
err := q.Run(ctx, func(a *Artifact) error {
actual = append(actual, a)
return nil
})
So(err, ShouldBeNil)
So(actual, ShouldHaveLength, 1)
So(actual[0].RBECASHash, ShouldEqual, "deadbeef")
})
Convey(`WithGcsURI`, func() {
testutil.MustApply(ctx,
insert.Artifact("inv1", "tr/t/r", "a", map[string]any{
"ContentType": "text/plain",
"Size": 64,
"GcsURI": "gs://bucket/beyondbeef",
}),
)
q.WithGcsURI = true
q.PageSize = 0
ctx, cancel := span.ReadOnlyTransaction(ctx)
defer cancel()
var actual []*Artifact
err := q.Run(ctx, func(a *Artifact) error {
actual = append(actual, a)
return nil
})
So(err, ShouldBeNil)
So(actual, ShouldHaveLength, 1)
So(actual[0].GcsUri, ShouldEqual, "gs://bucket/beyondbeef")
})
})
}