go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/resultdb/internal/services/bqexporter/bqexporter_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 bqexporter 16 17 import ( 18 "bytes" 19 "context" 20 "fmt" 21 "sync" 22 "testing" 23 24 "golang.org/x/sync/semaphore" 25 "golang.org/x/time/rate" 26 "google.golang.org/protobuf/types/known/structpb" 27 28 "go.chromium.org/luci/common/bq" 29 "go.chromium.org/luci/server/span" 30 "go.chromium.org/luci/server/tq" 31 32 artifactcontenttest "go.chromium.org/luci/resultdb/internal/artifactcontent/testutil" 33 "go.chromium.org/luci/resultdb/internal/spanutil" 34 "go.chromium.org/luci/resultdb/internal/tasks/taskspb" 35 "go.chromium.org/luci/resultdb/internal/testutil" 36 "go.chromium.org/luci/resultdb/internal/testutil/insert" 37 "go.chromium.org/luci/resultdb/pbutil" 38 bqpb "go.chromium.org/luci/resultdb/proto/bq" 39 pb "go.chromium.org/luci/resultdb/proto/v1" 40 41 . "github.com/smartystreets/goconvey/convey" 42 . "go.chromium.org/luci/common/testing/assertions" 43 ) 44 45 type mockPassInserter struct { 46 insertedMessages []*bq.Row 47 mu sync.Mutex 48 } 49 50 func (i *mockPassInserter) Put(ctx context.Context, src any) error { 51 messages := src.([]*bq.Row) 52 i.mu.Lock() 53 i.insertedMessages = append(i.insertedMessages, messages...) 54 i.mu.Unlock() 55 return nil 56 } 57 58 type mockFailInserter struct { 59 } 60 61 func (i *mockFailInserter) Put(ctx context.Context, src any) error { 62 return fmt.Errorf("some error") 63 } 64 65 func TestExportToBigQuery(t *testing.T) { 66 Convey(`TestExportTestResultsToBigQuery`, t, func() { 67 ctx := testutil.SpannerTestContext(t) 68 testutil.MustApply(ctx, 69 insert.Invocation("a", pb.Invocation_FINALIZED, map[string]any{ 70 "Realm": "testproject:testrealm", 71 "Sources": spanutil.Compressed(pbutil.MustMarshal(testutil.TestSources())), 72 }), 73 insert.Invocation("b", pb.Invocation_FINALIZED, map[string]any{ 74 "Realm": "testproject:testrealm", 75 "Properties": spanutil.Compressed(pbutil.MustMarshal(&structpb.Struct{ 76 Fields: map[string]*structpb.Value{ 77 "key": structpb.NewStringValue("value"), 78 }, 79 })), 80 "InheritSources": true, 81 }), 82 insert.Inclusion("a", "b")) 83 testutil.MustApply(ctx, testutil.CombineMutations( 84 // Test results and exonerations have the same variants. 85 insert.TestResults("a", "A", pbutil.Variant("k", "v"), pb.TestStatus_FAIL, pb.TestStatus_PASS), 86 insert.TestExonerations("a", "A", pbutil.Variant("k", "v"), pb.ExonerationReason_OCCURS_ON_OTHER_CLS), 87 // Test results and exonerations have different variants. 88 insert.TestResults("b", "B", pbutil.Variant("k", "v"), pb.TestStatus_CRASH, pb.TestStatus_PASS), 89 insert.TestExonerations("b", "B", pbutil.Variant("k", "different"), pb.ExonerationReason_OCCURS_ON_MAINLINE), 90 // Passing test result without exoneration. 91 insert.TestResults("a", "C", nil, pb.TestStatus_PASS), 92 // Test results' parent is different from exported. 93 insert.TestResults("b", "D", pbutil.Variant("k", "v"), pb.TestStatus_CRASH, pb.TestStatus_PASS), 94 insert.TestExonerations("b", "D", pbutil.Variant("k", "v"), pb.ExonerationReason_OCCURS_ON_OTHER_CLS), 95 insert.TestResultMessages([]*pb.TestResult{ 96 { 97 Name: pbutil.TestResultName("a", "E", "0"), 98 TestId: "E", 99 ResultId: "0", 100 Variant: pbutil.Variant("k2", "v2", "k3", "v3"), 101 VariantHash: pbutil.VariantHash(pbutil.Variant("k2", "v2", "k3", "v3")), 102 Expected: true, 103 Status: pb.TestStatus_SKIP, 104 SkipReason: pb.SkipReason_AUTOMATICALLY_DISABLED_FOR_FLAKINESS, 105 Properties: &structpb.Struct{ 106 Fields: map[string]*structpb.Value{ 107 "key_1": structpb.NewStringValue("value_1"), 108 "key_2": structpb.NewStructValue(&structpb.Struct{ 109 Fields: map[string]*structpb.Value{ 110 "child_key": structpb.NewNumberValue(1), 111 }, 112 }), 113 }, 114 }, 115 }, 116 }), 117 )...) 118 119 bqExport := &pb.BigQueryExport{ 120 Project: "project", 121 Dataset: "dataset", 122 Table: "table", 123 ResultType: &pb.BigQueryExport_TestResults_{ 124 TestResults: &pb.BigQueryExport_TestResults{}, 125 }, 126 } 127 128 opts := DefaultOptions() 129 b := &bqExporter{ 130 Options: &opts, 131 putLimiter: rate.NewLimiter(100, 1), 132 batchSem: semaphore.NewWeighted(100), 133 } 134 135 Convey(`success`, func() { 136 i := &mockPassInserter{} 137 err := b.exportTestResultsToBigQuery(ctx, i, "a", bqExport) 138 So(err, ShouldBeNil) 139 140 i.mu.Lock() 141 defer i.mu.Unlock() 142 So(len(i.insertedMessages), ShouldEqual, 8) 143 144 expectedTestIDs := []string{"A", "B", "C", "D", "E"} 145 for _, m := range i.insertedMessages { 146 tr := m.Message.(*bqpb.TestResultRow) 147 So(tr.TestId, ShouldBeIn, expectedTestIDs) 148 So(tr.Parent.Id, ShouldBeIn, []string{"a", "b"}) 149 So(tr.Parent.Realm, ShouldEqual, "testproject:testrealm") 150 if tr.Parent.Id == "b" { 151 So(tr.Parent.Properties, ShouldResembleProto, &structpb.Struct{ 152 Fields: map[string]*structpb.Value{ 153 "key": structpb.NewStringValue("value"), 154 }, 155 }) 156 } else { 157 So(tr.Parent.Properties, ShouldBeNil) 158 } 159 160 So(tr.Exported.Id, ShouldEqual, "a") 161 So(tr.Exported.Realm, ShouldEqual, "testproject:testrealm") 162 So(tr.Exported.Properties, ShouldBeNil) 163 So(tr.Exonerated, ShouldEqual, tr.TestId == "A" || tr.TestId == "D") 164 165 So(tr.Name, ShouldEqual, pbutil.TestResultName(string(tr.Parent.Id), tr.TestId, tr.ResultId)) 166 167 if tr.TestId == "E" { 168 So(tr.Properties, ShouldResembleProto, &structpb.Struct{ 169 Fields: map[string]*structpb.Value{ 170 "key_1": structpb.NewStringValue("value_1"), 171 "key_2": structpb.NewStructValue(&structpb.Struct{ 172 Fields: map[string]*structpb.Value{ 173 "child_key": structpb.NewNumberValue(1), 174 }, 175 }), 176 }, 177 }) 178 So(tr.SkipReason, ShouldEqual, pb.SkipReason_AUTOMATICALLY_DISABLED_FOR_FLAKINESS.String()) 179 } else { 180 So(tr.Properties, ShouldBeNil) 181 So(tr.SkipReason, ShouldBeEmpty) 182 } 183 184 So(tr.Sources, ShouldResembleProto, testutil.TestSources()) 185 } 186 }) 187 188 // To check when encountering an error, the test can run to the end 189 // without hanging, or race detector does not detect anything. 190 Convey(`fail`, func() { 191 err := b.exportTestResultsToBigQuery(ctx, &mockFailInserter{}, "a", bqExport) 192 So(err, ShouldErrLike, "some error") 193 }) 194 }) 195 196 Convey(`TestExportTextArtifactToBigQuery`, t, func() { 197 ctx := testutil.SpannerTestContext(t) 198 testutil.MustApply(ctx, 199 insert.Invocation("a", pb.Invocation_FINALIZED, map[string]any{"Realm": "testproject:testrealm"}), 200 insert.Invocation("inv1", pb.Invocation_FINALIZED, map[string]any{"Realm": "testproject:testrealm"}), 201 insert.Inclusion("a", "inv1"), 202 insert.Artifact("inv1", "", "a0", map[string]any{"ContentType": "text/plain; encoding=utf-8", "Size": "100", "RBECASHash": "deadbeef"}), 203 insert.Artifact("inv1", "tr/t/r", "a0", map[string]any{"ContentType": "text/plain", "Size": "100", "RBECASHash": "deadbeef"}), 204 insert.Artifact("inv1", "tr/t/r", "a1", nil), 205 insert.Artifact("inv1", "tr/t/r", "a2", map[string]any{"ContentType": "text/plain;encoding=ascii", "Size": "100", "RBECASHash": "deadbeef"}), 206 insert.Artifact("inv1", "tr/t/r", "a3", map[string]any{"ContentType": "image/jpg", "Size": "100"}), 207 insert.Artifact("inv1", "tr/t/r", "a4", map[string]any{"ContentType": "text/plain;encoding=utf-8", "Size": "100", "RBECASHash": "deadbeef"}), 208 ) 209 210 bqExport := &pb.BigQueryExport{ 211 Project: "project", 212 Dataset: "dataset", 213 Table: "table", 214 ResultType: &pb.BigQueryExport_TextArtifacts_{ 215 TextArtifacts: &pb.BigQueryExport_TextArtifacts{ 216 Predicate: &pb.ArtifactPredicate{}, 217 }, 218 }, 219 } 220 221 opts := DefaultOptions() 222 b := &bqExporter{ 223 Options: &opts, 224 putLimiter: rate.NewLimiter(100, 1), 225 batchSem: semaphore.NewWeighted(100), 226 rbecasClient: &artifactcontenttest.FakeByteStreamClient{ 227 ExtraResponseData: bytes.Repeat([]byte("short\ncontentspart2\n"), 200000), 228 }, 229 maxTokenSize: 10, 230 } 231 232 Convey(`success`, func() { 233 i := &mockPassInserter{} 234 err := b.exportTextArtifactsToBigQuery(ctx, i, "a", bqExport) 235 So(err, ShouldBeNil) 236 237 i.mu.Lock() 238 defer i.mu.Unlock() 239 So(len(i.insertedMessages), ShouldEqual, 8) 240 }) 241 242 Convey(`fail`, func() { 243 err := b.exportTextArtifactsToBigQuery(ctx, &mockFailInserter{}, "a", bqExport) 244 So(err, ShouldErrLike, "some error") 245 }) 246 }) 247 } 248 249 func TestSchedule(t *testing.T) { 250 Convey(`TestSchedule`, t, func() { 251 ctx := testutil.SpannerTestContext(t) 252 bqExport1 := &pb.BigQueryExport{Dataset: "dataset", Project: "project", Table: "table", ResultType: &pb.BigQueryExport_TestResults_{}} 253 bqExport2 := &pb.BigQueryExport{Dataset: "dataset2", Project: "project2", Table: "table2", ResultType: &pb.BigQueryExport_TextArtifacts_{}} 254 bqExports := []*pb.BigQueryExport{bqExport1, bqExport2} 255 testutil.MustApply(ctx, 256 insert.Invocation("two-bqx", pb.Invocation_FINALIZED, map[string]any{"BigqueryExports": bqExports}), 257 insert.Invocation("one-bqx", pb.Invocation_FINALIZED, map[string]any{"BigqueryExports": bqExports[:1]}), 258 insert.Invocation("zero-bqx", pb.Invocation_FINALIZED, nil)) 259 260 ctx, sched := tq.TestingContext(ctx, nil) 261 _, err := span.ReadWriteTransaction(ctx, func(ctx context.Context) error { 262 So(Schedule(ctx, "two-bqx"), ShouldBeNil) 263 So(Schedule(ctx, "one-bqx"), ShouldBeNil) 264 So(Schedule(ctx, "zero-bqx"), ShouldBeNil) 265 return nil 266 }) 267 So(err, ShouldBeNil) 268 So(sched.Tasks().Payloads()[0], ShouldResembleProto, &taskspb.ExportInvocationTestResultsToBQ{InvocationId: "one-bqx", BqExport: bqExport1}) 269 So(sched.Tasks().Payloads()[1], ShouldResembleProto, &taskspb.ExportInvocationArtifactsToBQ{InvocationId: "two-bqx", BqExport: bqExport2}) 270 So(sched.Tasks().Payloads()[2], ShouldResembleProto, &taskspb.ExportInvocationTestResultsToBQ{InvocationId: "two-bqx", BqExport: bqExport1}) 271 }) 272 }