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  }