go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cipd/appengine/impl/repo/processing/extractor_test.go (about)

     1  // Copyright 2021 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 processing
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"io"
    21  	"strings"
    22  	"testing"
    23  
    24  	"google.golang.org/grpc/codes"
    25  	"google.golang.org/grpc/status"
    26  
    27  	api "go.chromium.org/luci/cipd/api/cipd/v1"
    28  	"go.chromium.org/luci/cipd/appengine/impl/gs"
    29  	"go.chromium.org/luci/cipd/appengine/impl/testutil"
    30  	"go.chromium.org/luci/cipd/common"
    31  	"go.chromium.org/luci/common/retry/transient"
    32  
    33  	. "github.com/smartystreets/goconvey/convey"
    34  	. "go.chromium.org/luci/common/testing/assertions"
    35  )
    36  
    37  func TestExtractor(t *testing.T) {
    38  	t.Parallel()
    39  
    40  	ctx := context.Background()
    41  
    42  	Convey("With mocks", t, func() {
    43  		var publishedRef *api.ObjectRef
    44  		var canceled bool
    45  
    46  		expectedUploadAlgo := api.HashAlgo_SHA256
    47  
    48  		cas := testutil.MockCAS{
    49  			BeginUploadImpl: func(_ context.Context, r *api.BeginUploadRequest) (*api.UploadOperation, error) {
    50  				So(r.HashAlgo, ShouldEqual, expectedUploadAlgo)
    51  				return &api.UploadOperation{
    52  					OperationId: "op_id",
    53  					UploadUrl:   "http://example.com/upload",
    54  				}, nil
    55  			},
    56  			FinishUploadImpl: func(_ context.Context, r *api.FinishUploadRequest) (*api.UploadOperation, error) {
    57  				So(r.UploadOperationId, ShouldEqual, "op_id")
    58  				publishedRef = r.ForceHash
    59  				return &api.UploadOperation{Status: api.UploadStatus_PUBLISHED}, nil
    60  			},
    61  			CancelUploadImpl: func(_ context.Context, r *api.CancelUploadRequest) (*api.UploadOperation, error) {
    62  				So(r.UploadOperationId, ShouldEqual, "op_id")
    63  				canceled = true
    64  				return &api.UploadOperation{Status: api.UploadStatus_CANCELED}, nil
    65  			},
    66  		}
    67  
    68  		extracted := bytes.Buffer{}
    69  		uploader := &trackingWriter{w: &extracted}
    70  
    71  		testFileBody := strings.Repeat("01234567", 8960)
    72  
    73  		ex := Extractor{
    74  			Reader: packageReader(map[string]string{
    75  				"test/file": testFileBody,
    76  			}),
    77  			CAS:               &cas,
    78  			PrimaryHash:       api.HashAlgo_SHA256,
    79  			AlternativeHashes: []api.HashAlgo{api.HashAlgo_SHA1},
    80  			Uploader:          func(ctx context.Context, size int64, uploadURL string) io.Writer { return uploader },
    81  			BufferSize:        64 * 1024,
    82  		}
    83  
    84  		Convey("Happy path, SHA256", func() {
    85  			res, err := ex.Run(ctx, "test/file")
    86  			So(err, ShouldBeNil)
    87  
    88  			// Check the return value.
    89  			So(res.Path, ShouldEqual, "test/file")
    90  			So(res.Ref, ShouldResembleProto, &api.ObjectRef{
    91  				HashAlgo:  api.HashAlgo_SHA256,
    92  				HexDigest: hexDigest(api.HashAlgo_SHA256, testFileBody),
    93  			})
    94  			So(res.Size, ShouldEqual, len(testFileBody))
    95  			So(res.Hashes, ShouldHaveLength, 2)
    96  			for _, h := range []api.HashAlgo{api.HashAlgo_SHA1, api.HashAlgo_SHA256} {
    97  				So(common.HexDigest(res.Hashes[h]), ShouldEqual, hexDigest(h, testFileBody))
    98  			}
    99  
   100  			// Check it actually uploaded the correct thing.
   101  			So(extracted.String(), ShouldEqual, testFileBody)
   102  			So(publishedRef, ShouldResembleProto, &api.ObjectRef{
   103  				HashAlgo:  api.HashAlgo_SHA256,
   104  				HexDigest: hexDigest(api.HashAlgo_SHA256, testFileBody),
   105  			})
   106  
   107  			// Check it was written in 64 Kb chunks, NOT 32 Kb as used by zip.Reader.
   108  			So(uploader.calls, ShouldResemble, []int{64 * 1024, 6 * 1024})
   109  		})
   110  
   111  		Convey("No such file in the package", func() {
   112  			_, err := ex.Run(ctx, "unknown")
   113  			So(err, ShouldErrLike, `failed to open the file for reading: no file "unknown" inside the package`)
   114  			So(transient.Tag.In(err), ShouldBeFalse)
   115  		})
   116  
   117  		Convey("Internal error when initiating the upload", func() {
   118  			cas.BeginUploadImpl = func(_ context.Context, r *api.BeginUploadRequest) (*api.UploadOperation, error) {
   119  				return nil, status.Errorf(codes.Internal, "boo")
   120  			}
   121  			_, err := ex.Run(ctx, "test/file")
   122  			So(err, ShouldErrLike, `failed to open a CAS upload: rpc error: code = Internal desc = boo`)
   123  			So(transient.Tag.In(err), ShouldBeTrue)
   124  		})
   125  
   126  		Convey("Internal error when finalizing the upload", func() {
   127  			cas.FinishUploadImpl = func(_ context.Context, r *api.FinishUploadRequest) (*api.UploadOperation, error) {
   128  				return nil, status.Errorf(codes.Internal, "boo")
   129  			}
   130  			_, err := ex.Run(ctx, "test/file")
   131  			So(err, ShouldErrLike, `failed to finalize the CAS upload: rpc error: code = Internal desc = boo`)
   132  			So(transient.Tag.In(err), ShouldBeTrue)
   133  		})
   134  
   135  		Convey("Asked to restart the upload", func() {
   136  			uploader.err = &gs.RestartUploadError{Offset: 124}
   137  
   138  			_, err := ex.Run(ctx, "test/file")
   139  			So(err, ShouldErrLike, `asked to restart the upload from faraway offset: the upload should be restarted from offset 124`)
   140  			So(transient.Tag.In(err), ShouldBeTrue)
   141  			So(canceled, ShouldBeTrue)
   142  		})
   143  	})
   144  }
   145  
   146  func packageReader(data map[string]string) *PackageReader {
   147  	buf := bytes.NewReader(testutil.MakeZip(data))
   148  	size := int64(buf.Len())
   149  	r, _ := NewPackageReader(buf, size)
   150  	return r
   151  }
   152  
   153  type trackingWriter struct {
   154  	w     io.Writer
   155  	calls []int // sizes of each pushed chunk
   156  	err   error // err to return from Write
   157  }
   158  
   159  func (w *trackingWriter) Write(p []byte) (int, error) {
   160  	if w.err != nil {
   161  		return 0, w.err
   162  	}
   163  	w.calls = append(w.calls, len(p))
   164  	return w.w.Write(p)
   165  }