github.com/bazelbuild/remote-apis-sdks@v0.0.0-20240425170053-8a36686a6350/go/pkg/cas/upload_test.go (about)

     1  package cas
     2  
     3  import (
     4  	"context"
     5  	"os"
     6  	"path/filepath"
     7  	"regexp"
     8  	"sort"
     9  	"strings"
    10  	"sync"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/google/go-cmp/cmp"
    15  	"github.com/pkg/errors"
    16  	"google.golang.org/grpc"
    17  	"google.golang.org/grpc/codes"
    18  	"google.golang.org/grpc/status"
    19  	"google.golang.org/protobuf/proto"
    20  
    21  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/digest"
    22  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/fakes"
    23  	// Redundant imports are required for the google3 mirror. Aliases should not be changed.
    24  	regrpc "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2"
    25  	repb "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2"
    26  )
    27  
    28  func TestFS(t *testing.T) {
    29  	t.Parallel()
    30  	ctx := context.Background()
    31  
    32  	tmpDir := t.TempDir()
    33  	putFile(t, filepath.Join(tmpDir, "root", "a"), "a")
    34  	aItem := uploadItemFromBlob(filepath.Join(tmpDir, "root", "a"), []byte("a"))
    35  
    36  	putFile(t, filepath.Join(tmpDir, "root", "b"), "b")
    37  	bItem := uploadItemFromBlob(filepath.Join(tmpDir, "root", "b"), []byte("b"))
    38  
    39  	putFile(t, filepath.Join(tmpDir, "root", "subdir", "c"), "c")
    40  	cItem := uploadItemFromBlob(filepath.Join(tmpDir, "root", "subdir", "c"), []byte("c"))
    41  
    42  	putFile(t, filepath.Join(tmpDir, "root", "subdir", "d"), "d")
    43  	dItem := uploadItemFromBlob(filepath.Join(tmpDir, "root", "subdir", "d"), []byte("d"))
    44  
    45  	subdirItem := uploadItemFromDirMsg(filepath.Join(tmpDir, "root", "subdir"), &repb.Directory{
    46  		Files: []*repb.FileNode{
    47  			{
    48  				Name:   "c",
    49  				Digest: cItem.Digest,
    50  			},
    51  			{
    52  				Name:   "d",
    53  				Digest: dItem.Digest,
    54  			},
    55  		},
    56  	})
    57  	subdirWithoutDItem := uploadItemFromDirMsg(filepath.Join(tmpDir, "root", "subdir"), &repb.Directory{
    58  		Files: []*repb.FileNode{
    59  			{
    60  				Name:   "c",
    61  				Digest: cItem.Digest,
    62  			},
    63  		},
    64  	})
    65  
    66  	rootItem := uploadItemFromDirMsg(filepath.Join(tmpDir, "root"), &repb.Directory{
    67  		Files: []*repb.FileNode{
    68  			{Name: "a", Digest: aItem.Digest},
    69  			{Name: "b", Digest: bItem.Digest},
    70  		},
    71  		Directories: []*repb.DirectoryNode{
    72  			{Name: "subdir", Digest: subdirItem.Digest},
    73  		},
    74  	})
    75  	rootWithoutAItem := uploadItemFromDirMsg(filepath.Join(tmpDir, "root"), &repb.Directory{
    76  		Files: []*repb.FileNode{
    77  			{Name: "b", Digest: bItem.Digest},
    78  		},
    79  		Directories: []*repb.DirectoryNode{
    80  			{Name: "subdir", Digest: subdirItem.Digest},
    81  		},
    82  	})
    83  	rootWithoutSubdirItem := uploadItemFromDirMsg(filepath.Join(tmpDir, "root"), &repb.Directory{
    84  		Files: []*repb.FileNode{
    85  			{Name: "a", Digest: aItem.Digest},
    86  			{Name: "b", Digest: bItem.Digest},
    87  		},
    88  	})
    89  	rootWithoutDItem := uploadItemFromDirMsg(filepath.Join(tmpDir, "root"), &repb.Directory{
    90  		Files: []*repb.FileNode{
    91  			{Name: "a", Digest: aItem.Digest},
    92  			{Name: "b", Digest: bItem.Digest},
    93  		},
    94  		Directories: []*repb.DirectoryNode{
    95  			{Name: "subdir", Digest: subdirWithoutDItem.Digest},
    96  		},
    97  	})
    98  
    99  	putFile(t, filepath.Join(tmpDir, "medium-dir", "medium"), "medium")
   100  	mediumItem := uploadItemFromBlob(filepath.Join(tmpDir, "medium-dir", "medium"), []byte("medium"))
   101  	mediumDirItem := uploadItemFromDirMsg(filepath.Join(tmpDir, "medium-dir"), &repb.Directory{
   102  		Files: []*repb.FileNode{{
   103  			Name:   "medium",
   104  			Digest: mediumItem.Digest,
   105  		}},
   106  	})
   107  
   108  	putSymlink(t, filepath.Join(tmpDir, "with-symlinks", "file"), filepath.Join("..", "root", "a"))
   109  	putSymlink(t, filepath.Join(tmpDir, "with-symlinks", "dir"), filepath.Join("..", "root", "subdir"))
   110  	withSymlinksItemPreserved := uploadItemFromDirMsg(filepath.Join(tmpDir, "with-symlinks"), &repb.Directory{
   111  		Symlinks: []*repb.SymlinkNode{
   112  			{
   113  				Name:   "file",
   114  				Target: "../root/a",
   115  			},
   116  			{
   117  				Name:   "dir",
   118  				Target: "../root/subdir",
   119  			},
   120  		},
   121  	})
   122  
   123  	withSymlinksItemNotPreserved := uploadItemFromDirMsg(filepath.Join(tmpDir, "with-symlinks"), &repb.Directory{
   124  		Files: []*repb.FileNode{
   125  			{Name: "a", Digest: aItem.Digest},
   126  		},
   127  		Directories: []*repb.DirectoryNode{
   128  			{Name: "subdir", Digest: subdirItem.Digest},
   129  		},
   130  	})
   131  
   132  	putSymlink(t, filepath.Join(tmpDir, "with-dangling-symlink", "dangling"), "non-existent")
   133  	withDanglingSymlinksItem := uploadItemFromDirMsg(filepath.Join(tmpDir, "with-dangling-symlink"), &repb.Directory{
   134  		Symlinks: []*repb.SymlinkNode{
   135  			{Name: "dangling", Target: "non-existent"},
   136  		},
   137  	})
   138  
   139  	digSlice := func(items ...*uploadItem) []digest.Digest {
   140  		ret := make([]digest.Digest, len(items))
   141  		for i, item := range items {
   142  			ret[i] = digest.NewFromProtoUnvalidated(item.Digest)
   143  		}
   144  		return ret
   145  	}
   146  
   147  	tests := []struct {
   148  		desc                string
   149  		inputs              []*UploadInput
   150  		wantDigests         []digest.Digest
   151  		wantScheduledChecks []*uploadItem
   152  		wantErr             error
   153  		opt                 UploadOptions
   154  	}{
   155  		{
   156  			desc:                "root",
   157  			inputs:              []*UploadInput{{Path: filepath.Join(tmpDir, "root")}},
   158  			wantDigests:         digSlice(rootItem),
   159  			wantScheduledChecks: []*uploadItem{rootItem, aItem, bItem, subdirItem, cItem, dItem},
   160  		},
   161  		{
   162  			desc:        "root-without-a-using-callback",
   163  			inputs:      []*UploadInput{{Path: filepath.Join(tmpDir, "root")}},
   164  			wantDigests: digSlice(rootWithoutAItem),
   165  			opt: UploadOptions{
   166  				Prelude: func(absPath string, mode os.FileMode) error {
   167  					if filepath.Base(absPath) == "a" {
   168  						return ErrSkip
   169  					}
   170  					return nil
   171  				},
   172  			},
   173  			wantScheduledChecks: []*uploadItem{rootWithoutAItem, bItem, subdirItem, cItem, dItem},
   174  		},
   175  		{
   176  			desc:                "root-without-a-using-allowlist",
   177  			inputs:              []*UploadInput{{Path: filepath.Join(tmpDir, "root"), Allowlist: []string{"b", "subdir"}}},
   178  			wantDigests:         digSlice(rootWithoutAItem),
   179  			wantScheduledChecks: []*uploadItem{rootWithoutAItem, bItem, subdirItem, cItem, dItem},
   180  		},
   181  		{
   182  			desc:                "root-without-subdir-using-allowlist",
   183  			inputs:              []*UploadInput{{Path: filepath.Join(tmpDir, "root"), Allowlist: []string{"a", "b"}}},
   184  			wantDigests:         digSlice(rootWithoutSubdirItem),
   185  			wantScheduledChecks: []*uploadItem{rootWithoutSubdirItem, aItem, bItem},
   186  		},
   187  		{
   188  			desc:                "root-without-d-using-allowlist",
   189  			inputs:              []*UploadInput{{Path: filepath.Join(tmpDir, "root"), Allowlist: []string{"a", "b", filepath.Join("subdir", "c")}}},
   190  			wantDigests:         digSlice(rootWithoutDItem),
   191  			wantScheduledChecks: []*uploadItem{rootWithoutDItem, aItem, bItem, subdirWithoutDItem, cItem},
   192  		},
   193  		{
   194  			desc: "root-without-b-using-exclude",
   195  			inputs: []*UploadInput{{
   196  				Path:    filepath.Join(tmpDir, "root"),
   197  				Exclude: regexp.MustCompile(`[/\\]a$`),
   198  			}},
   199  			wantDigests:         digSlice(rootWithoutAItem),
   200  			wantScheduledChecks: []*uploadItem{rootWithoutAItem, bItem, subdirItem, cItem, dItem},
   201  		},
   202  		{
   203  			desc: "same-regular-file-is-read-only-once",
   204  			// The two regexps below do not exclude anything.
   205  			// This test ensures that same files aren't checked twice.
   206  			inputs: []*UploadInput{
   207  				{
   208  					Path:    filepath.Join(tmpDir, "root"),
   209  					Exclude: regexp.MustCompile(`1$`),
   210  				},
   211  				{
   212  					Path:    filepath.Join(tmpDir, "root"),
   213  					Exclude: regexp.MustCompile(`2$`),
   214  				},
   215  			},
   216  			// OnDigest is called for each UploadItem separately.
   217  			wantDigests: digSlice(rootItem, rootItem),
   218  			// Directories are checked twice, but files are checked only once.
   219  			wantScheduledChecks: []*uploadItem{rootItem, rootItem, aItem, bItem, subdirItem, subdirItem, cItem, dItem},
   220  		},
   221  		{
   222  			desc:   "root-without-subdir",
   223  			inputs: []*UploadInput{{Path: filepath.Join(tmpDir, "root")}},
   224  			opt: UploadOptions{
   225  				Prelude: func(absPath string, mode os.FileMode) error {
   226  					if strings.Contains(absPath, "subdir") {
   227  						return ErrSkip
   228  					}
   229  					return nil
   230  				},
   231  			},
   232  			wantDigests:         digSlice(rootWithoutSubdirItem),
   233  			wantScheduledChecks: []*uploadItem{rootWithoutSubdirItem, aItem, bItem},
   234  		},
   235  		{
   236  			desc:                "medium",
   237  			inputs:              []*UploadInput{{Path: filepath.Join(tmpDir, "medium-dir")}},
   238  			wantDigests:         digSlice(mediumDirItem),
   239  			wantScheduledChecks: []*uploadItem{mediumDirItem, mediumItem},
   240  		},
   241  		{
   242  			desc:                "symlinks-preserved",
   243  			opt:                 UploadOptions{PreserveSymlinks: true},
   244  			inputs:              []*UploadInput{{Path: filepath.Join(tmpDir, "with-symlinks")}},
   245  			wantDigests:         digSlice(withSymlinksItemPreserved),
   246  			wantScheduledChecks: []*uploadItem{withSymlinksItemPreserved},
   247  		},
   248  		{
   249  			desc:                "symlinks-not-preserved",
   250  			inputs:              []*UploadInput{{Path: filepath.Join(tmpDir, "with-symlinks")}},
   251  			wantDigests:         digSlice(withSymlinksItemNotPreserved),
   252  			wantScheduledChecks: []*uploadItem{aItem, subdirItem, cItem, dItem, withSymlinksItemNotPreserved},
   253  		},
   254  		{
   255  			desc:    "dangling-symlinks-disallow",
   256  			inputs:  []*UploadInput{{Path: filepath.Join(tmpDir, "with-dangling-symlinks")}},
   257  			wantErr: os.ErrNotExist,
   258  		},
   259  		{
   260  			desc:                "dangling-symlinks-allow",
   261  			opt:                 UploadOptions{PreserveSymlinks: true, AllowDanglingSymlinks: true},
   262  			inputs:              []*UploadInput{{Path: filepath.Join(tmpDir, "with-dangling-symlink")}},
   263  			wantDigests:         digSlice(withDanglingSymlinksItem),
   264  			wantScheduledChecks: []*uploadItem{withDanglingSymlinksItem},
   265  		},
   266  		{
   267  			desc: "dangling-symlink-via-filtering",
   268  			opt:  UploadOptions{PreserveSymlinks: true},
   269  			inputs: []*UploadInput{{
   270  				Path:    filepath.Join(tmpDir, "with-symlinks"),
   271  				Exclude: regexp.MustCompile("root"),
   272  			}},
   273  			wantDigests:         digSlice(withSymlinksItemPreserved),
   274  			wantScheduledChecks: []*uploadItem{withSymlinksItemPreserved},
   275  		},
   276  		{
   277  			desc: "dangling-symlink-via-filtering-allow",
   278  			opt:  UploadOptions{PreserveSymlinks: true, AllowDanglingSymlinks: true},
   279  			inputs: []*UploadInput{{
   280  				Path:    filepath.Join(tmpDir, "with-symlinks"),
   281  				Exclude: regexp.MustCompile("root"),
   282  			}},
   283  			wantDigests:         digSlice(withSymlinksItemPreserved),
   284  			wantScheduledChecks: []*uploadItem{withSymlinksItemPreserved},
   285  		},
   286  	}
   287  
   288  	for _, tc := range tests {
   289  		t.Run(tc.desc, func(t *testing.T) {
   290  			var mu sync.Mutex
   291  			var gotScheduledChecks []*uploadItem
   292  
   293  			client := &Client{
   294  				Config: DefaultClientConfig(),
   295  				testScheduleCheck: func(ctx context.Context, item *uploadItem) error {
   296  					mu.Lock()
   297  					defer mu.Unlock()
   298  					gotScheduledChecks = append(gotScheduledChecks, item)
   299  					return nil
   300  				},
   301  			}
   302  			client.Config.SmallFileThreshold = 5
   303  			client.Config.LargeFileThreshold = 10
   304  			client.init()
   305  
   306  			_, err := client.Upload(ctx, tc.opt, uploadInputChanFrom(tc.inputs...))
   307  			if tc.wantErr != nil {
   308  				if !errors.Is(err, tc.wantErr) {
   309  					t.Fatalf("error mismatch: want %q, got %q", tc.wantErr, err)
   310  				}
   311  				return
   312  			}
   313  			if err != nil {
   314  				t.Fatal(err)
   315  			}
   316  
   317  			sort.Slice(gotScheduledChecks, func(i, j int) bool {
   318  				return gotScheduledChecks[i].Title < gotScheduledChecks[j].Title
   319  			})
   320  			if diff := cmp.Diff(tc.wantScheduledChecks, gotScheduledChecks, cmp.Comparer(compareUploadItems)); diff != "" {
   321  				t.Errorf("unexpected scheduled checks (-want +got):\n%s", diff)
   322  			}
   323  
   324  			gotDigests := make([]digest.Digest, 0, len(tc.inputs))
   325  			for _, in := range tc.inputs {
   326  				dig, err := in.Digest(".")
   327  				if err != nil {
   328  					t.Errorf("UploadResult.Digest(%#v) failed: %s", in.Path, err)
   329  				} else {
   330  					gotDigests = append(gotDigests, dig)
   331  				}
   332  			}
   333  			if diff := cmp.Diff(tc.wantDigests, gotDigests); diff != "" {
   334  				t.Errorf("unexpected digests (-want +got):\n%s", diff)
   335  			}
   336  		})
   337  	}
   338  }
   339  
   340  func TestDigest(t *testing.T) {
   341  	t.Parallel()
   342  	ctx := context.Background()
   343  
   344  	tmpDir := t.TempDir()
   345  	putFile(t, filepath.Join(tmpDir, "root", "a"), "a")
   346  	putFile(t, filepath.Join(tmpDir, "root", "b"), "b")
   347  	putFile(t, filepath.Join(tmpDir, "root", "subdir", "c"), "c")
   348  	putFile(t, filepath.Join(tmpDir, "root", "subdir", "d"), "d")
   349  
   350  	e, cleanup := fakes.NewTestEnv(t)
   351  	defer cleanup()
   352  	conn, err := e.Server.NewClientConn(ctx)
   353  	if err != nil {
   354  		t.Fatal(err)
   355  	}
   356  
   357  	client, err := NewClientWithConfig(ctx, conn, "instance", DefaultClientConfig())
   358  	if err != nil {
   359  		t.Fatal(err)
   360  	}
   361  
   362  	inputs := []struct {
   363  		input       *UploadInput
   364  		wantDigests map[string]digest.Digest
   365  	}{
   366  		{
   367  			input: &UploadInput{
   368  				Path:      filepath.Join(tmpDir, "root"),
   369  				Allowlist: []string{"a", "b", filepath.Join("subdir", "c")},
   370  			},
   371  			wantDigests: map[string]digest.Digest{
   372  				".":      {Hash: "9a0af914385de712675cd780ae2dcb5e17b8943dc62cf9fc6fbf8ccd6f8c940d", Size: 230},
   373  				"a":      {Hash: "ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb", Size: 1},
   374  				"subdir": {Hash: "2d5c8ba78600fcadae65bab790bdf1f6f88278ec4abe1dc3aa7c26e60137dfc8", Size: 75},
   375  			},
   376  		},
   377  		{
   378  			input: &UploadInput{
   379  				Path:      filepath.Join(tmpDir, "root"),
   380  				Allowlist: []string{"a", "b", filepath.Join("subdir", "d")},
   381  			},
   382  			wantDigests: map[string]digest.Digest{
   383  				".":      {Hash: "2ab9cc3c9d504c883a66da62b57eb44fc9ca57abe05e75633b435e017920d8df", Size: 230},
   384  				"a":      {Hash: "ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb", Size: 1},
   385  				"subdir": {Hash: "ce33c7475f9ff2f2ee501eafcb2f21825b24a63de6fbabf7fbb886d606a448b9", Size: 75},
   386  			},
   387  		},
   388  	}
   389  
   390  	uploadInputs := make([]*UploadInput, len(inputs))
   391  	for i, in := range inputs {
   392  		uploadInputs[i] = in.input
   393  		if in.input.DigestsComputed() == nil {
   394  			t.Fatalf("DigestCopmuted() returned nil")
   395  		}
   396  	}
   397  
   398  	if _, err := client.Upload(ctx, UploadOptions{}, uploadInputChanFrom(uploadInputs...)); err != nil {
   399  		t.Fatal(err)
   400  	}
   401  
   402  	for i, in := range inputs {
   403  		t.Logf("input %d", i)
   404  		select {
   405  		case <-in.input.DigestsComputed():
   406  			// Good
   407  		case <-time.After(time.Second):
   408  			t.Errorf("Upload succeeded, but DigestsComputed() is not closed")
   409  		}
   410  
   411  		for relPath, wantDig := range in.wantDigests {
   412  			gotDig, err := in.input.Digest(relPath)
   413  			if err != nil {
   414  				t.Error(err)
   415  				continue
   416  			}
   417  			if diff := cmp.Diff(gotDig, wantDig); diff != "" {
   418  				t.Errorf("unexpected digest for %s (-want +got):\n%s", relPath, diff)
   419  			}
   420  		}
   421  	}
   422  }
   423  func TestSmallFiles(t *testing.T) {
   424  	t.Parallel()
   425  	ctx := context.Background()
   426  
   427  	var mu sync.Mutex
   428  	var gotDigestChecks []*repb.Digest
   429  	var gotDigestCheckRequestSizes []int
   430  	var gotUploadBlobReqs []*repb.BatchUpdateBlobsRequest_Request
   431  	var missing []*repb.Digest
   432  	failFirstMissing := true
   433  	cas := &fakeCAS{
   434  		findMissingBlobs: func(ctx context.Context, in *repb.FindMissingBlobsRequest, opts ...grpc.CallOption) (*repb.FindMissingBlobsResponse, error) {
   435  			mu.Lock()
   436  			defer mu.Unlock()
   437  			gotDigestChecks = append(gotDigestChecks, in.BlobDigests...)
   438  			gotDigestCheckRequestSizes = append(gotDigestCheckRequestSizes, len(in.BlobDigests))
   439  			missing = append(missing, in.BlobDigests[0])
   440  			return &repb.FindMissingBlobsResponse{MissingBlobDigests: in.BlobDigests[:1]}, nil
   441  		},
   442  		batchUpdateBlobs: func(ctx context.Context, in *repb.BatchUpdateBlobsRequest, opts ...grpc.CallOption) (*repb.BatchUpdateBlobsResponse, error) {
   443  			mu.Lock()
   444  			defer mu.Unlock()
   445  
   446  			gotUploadBlobReqs = append(gotUploadBlobReqs, in.Requests...)
   447  
   448  			res := &repb.BatchUpdateBlobsResponse{
   449  				Responses: make([]*repb.BatchUpdateBlobsResponse_Response, len(in.Requests)),
   450  			}
   451  			for i, r := range in.Requests {
   452  				res.Responses[i] = &repb.BatchUpdateBlobsResponse_Response{Digest: r.Digest}
   453  				if proto.Equal(r.Digest, missing[0]) && failFirstMissing {
   454  					res.Responses[i].Status = status.New(codes.Internal, "internal retrible error").Proto()
   455  					failFirstMissing = false
   456  				}
   457  			}
   458  			return res, nil
   459  		},
   460  	}
   461  	client := &Client{
   462  		InstanceName: "projects/p/instances/i",
   463  		Config:       DefaultClientConfig(),
   464  		cas:          cas,
   465  	}
   466  	client.Config.FindMissingBlobs.MaxItems = 2
   467  	client.init()
   468  
   469  	tmpDir := t.TempDir()
   470  	putFile(t, filepath.Join(tmpDir, "a"), "a")
   471  	putFile(t, filepath.Join(tmpDir, "b"), "b")
   472  	putFile(t, filepath.Join(tmpDir, "c"), "c")
   473  	putFile(t, filepath.Join(tmpDir, "d"), "d")
   474  	inputC := uploadInputChanFrom(
   475  		&UploadInput{Path: filepath.Join(tmpDir, "a")},
   476  		&UploadInput{Path: filepath.Join(tmpDir, "b")},
   477  		&UploadInput{Path: filepath.Join(tmpDir, "c")},
   478  		&UploadInput{Path: filepath.Join(tmpDir, "d")},
   479  	)
   480  	if _, err := client.Upload(ctx, UploadOptions{}, inputC); err != nil {
   481  		t.Fatalf("failed to upload: %s", err)
   482  	}
   483  
   484  	wantDigestChecks := []*repb.Digest{
   485  		{Hash: "18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4", SizeBytes: 1},
   486  		{Hash: "2e7d2c03a9507ae265ecf5b5356885a53393a2029d241394997265a1a25aefc6", SizeBytes: 1},
   487  		{Hash: "3e23e8160039594a33894f6564e1b1348bbd7a0088d42c4acb73eeaed59c009d", SizeBytes: 1},
   488  		{Hash: "ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb", SizeBytes: 1},
   489  	}
   490  	sort.Slice(gotDigestChecks, func(i, j int) bool {
   491  		return gotDigestChecks[i].Hash < gotDigestChecks[j].Hash
   492  	})
   493  	if diff := cmp.Diff(wantDigestChecks, gotDigestChecks, cmp.Comparer(proto.Equal)); diff != "" {
   494  		t.Error(diff)
   495  	}
   496  	if diff := cmp.Diff([]int{2, 2}, gotDigestCheckRequestSizes); diff != "" {
   497  		t.Error(diff)
   498  	}
   499  
   500  	if len(missing) != 2 {
   501  		t.Fatalf("want 2 missing, got %d", len(missing))
   502  	}
   503  	var wantUploadBlobsReqs []*repb.BatchUpdateBlobsRequest_Request
   504  	for _, blob := range []string{"a", "b", "c", "d"} {
   505  		blobBytes := []byte(blob)
   506  		req := &repb.BatchUpdateBlobsRequest_Request{Data: blobBytes, Digest: digest.NewFromBlob(blobBytes).ToProto()}
   507  		switch {
   508  		case proto.Equal(req.Digest, missing[0]):
   509  			wantUploadBlobsReqs = append(wantUploadBlobsReqs, req, req)
   510  		case proto.Equal(req.Digest, missing[1]):
   511  			wantUploadBlobsReqs = append(wantUploadBlobsReqs, req)
   512  		}
   513  
   514  	}
   515  	sort.Slice(wantUploadBlobsReqs, func(i, j int) bool {
   516  		return wantUploadBlobsReqs[i].Digest.Hash < wantUploadBlobsReqs[j].Digest.Hash
   517  	})
   518  	sort.Slice(gotUploadBlobReqs, func(i, j int) bool {
   519  		return gotUploadBlobReqs[i].Digest.Hash < gotUploadBlobReqs[j].Digest.Hash
   520  	})
   521  	if diff := cmp.Diff(wantUploadBlobsReqs, gotUploadBlobReqs, cmp.Comparer(proto.Equal)); diff != "" {
   522  		t.Error(diff)
   523  	}
   524  }
   525  
   526  func TestStreaming(t *testing.T) {
   527  	t.Parallel()
   528  	ctx := context.Background()
   529  
   530  	// TODO(nodir): add tests for retries.
   531  
   532  	e, cleanup := fakes.NewTestEnv(t)
   533  	defer cleanup()
   534  	conn, err := e.Server.NewClientConn(ctx)
   535  	if err != nil {
   536  		t.Fatal(err)
   537  	}
   538  
   539  	cfg := DefaultClientConfig()
   540  	cfg.BatchUpdateBlobs.MaxSizeBytes = 1
   541  	cfg.ByteStreamWrite.MaxSizeBytes = 2 // force multiple requests in a stream
   542  	cfg.SmallFileThreshold = 2
   543  	cfg.LargeFileThreshold = 3
   544  	cfg.CompressedBytestreamThreshold = 7 // between medium and large
   545  	client, err := NewClientWithConfig(ctx, conn, "instance", cfg)
   546  	if err != nil {
   547  		t.Fatal(err)
   548  	}
   549  
   550  	tmpDir := t.TempDir()
   551  	largeFilePath := filepath.Join(tmpDir, "testdata", "large")
   552  	putFile(t, largeFilePath, "laaaaaaaaaaarge")
   553  
   554  	res, err := client.Upload(ctx, UploadOptions{}, uploadInputChanFrom(
   555  		&UploadInput{Path: largeFilePath}, // large file
   556  	))
   557  	if err != nil {
   558  		t.Fatalf("failed to upload: %s", err)
   559  	}
   560  
   561  	cas := e.Server.CAS
   562  	if cas.WriteReqs() != 1 {
   563  		t.Errorf("want 1 write requests, got %d", cas.WriteReqs())
   564  	}
   565  
   566  	fileDigest := digest.Digest{Hash: "71944dd83e7e86354c3a9284e299e0d76c0b1108be62c8e7cefa72adf22128bf", Size: 15}
   567  	if got := cas.BlobWrites(fileDigest); got != 1 {
   568  		t.Errorf("want 1 write of %s, got %d", fileDigest, got)
   569  	}
   570  
   571  	wantStats := &TransferStats{
   572  		CacheMisses: DigestStat{Digests: 1, Bytes: 15},
   573  		Streamed:    DigestStat{Digests: 1, Bytes: 15},
   574  	}
   575  	if diff := cmp.Diff(wantStats, &res.Stats); diff != "" {
   576  		t.Errorf("unexpected stats (-want +got):\n%s", diff)
   577  	}
   578  
   579  	// Upload the large file again.
   580  	if _, err := client.Upload(ctx, UploadOptions{}, uploadInputChanFrom(&UploadInput{Path: largeFilePath})); err != nil {
   581  		t.Fatalf("failed to upload: %s", err)
   582  	}
   583  }
   584  
   585  func TestPartialMerkleTree(t *testing.T) {
   586  	t.Parallel()
   587  
   588  	mustDigest := func(m proto.Message) *repb.Digest {
   589  		d, err := digest.NewFromMessage(m)
   590  		if err != nil {
   591  			t.Fatal(err)
   592  		}
   593  		return d.ToProto()
   594  	}
   595  
   596  	type testCase struct {
   597  		tree      map[string]*digested
   598  		wantItems []*uploadItem
   599  	}
   600  
   601  	test := func(t *testing.T, tc testCase) {
   602  		in := &UploadInput{
   603  			tree:      tc.tree,
   604  			cleanPath: "/",
   605  		}
   606  		gotItems := in.partialMerkleTree()
   607  		sort.Slice(gotItems, func(i, j int) bool {
   608  			return gotItems[i].Title < gotItems[j].Title
   609  		})
   610  
   611  		if diff := cmp.Diff(tc.wantItems, gotItems, cmp.Comparer(compareUploadItems)); diff != "" {
   612  			t.Errorf("unexpected digests (-want +got):\n%s", diff)
   613  		}
   614  	}
   615  
   616  	t.Run("works", func(t *testing.T) {
   617  		barDigest := digest.NewFromBlob([]byte("bar")).ToProto()
   618  		bazDigest := mustDigest(&repb.Directory{})
   619  
   620  		foo := &repb.Directory{
   621  			Files: []*repb.FileNode{{
   622  				Name:   "bar",
   623  				Digest: barDigest,
   624  			}},
   625  			Directories: []*repb.DirectoryNode{{
   626  				Name:   "baz",
   627  				Digest: bazDigest,
   628  			}},
   629  		}
   630  
   631  		root := &repb.Directory{
   632  			Directories: []*repb.DirectoryNode{{
   633  				Name:   "foo",
   634  				Digest: mustDigest(foo),
   635  			}},
   636  		}
   637  
   638  		test(t, testCase{
   639  			tree: map[string]*digested{
   640  				"foo/bar": {
   641  					dirEntry: &repb.FileNode{
   642  						Name:   "bar",
   643  						Digest: barDigest,
   644  					},
   645  					digest: barDigest,
   646  				},
   647  				"foo/baz": {
   648  					dirEntry: &repb.DirectoryNode{
   649  						Name:   "baz",
   650  						Digest: bazDigest,
   651  					},
   652  					digest: bazDigest,
   653  				},
   654  			},
   655  			wantItems: []*uploadItem{
   656  				uploadItemFromDirMsg("/", root),
   657  				uploadItemFromDirMsg("/foo", foo),
   658  			},
   659  		})
   660  	})
   661  
   662  	t.Run("redundant info in the tree", func(t *testing.T) {
   663  		barDigest := mustDigest(&repb.Directory{})
   664  		barNode := &repb.DirectoryNode{
   665  			Name:   "bar",
   666  			Digest: barDigest,
   667  		}
   668  		foo := &repb.Directory{
   669  			Directories: []*repb.DirectoryNode{barNode},
   670  		}
   671  		root := &repb.Directory{
   672  			Directories: []*repb.DirectoryNode{{
   673  				Name:   "foo",
   674  				Digest: mustDigest(foo),
   675  			}},
   676  		}
   677  
   678  		test(t, testCase{
   679  			tree: map[string]*digested{
   680  				"foo/bar": {dirEntry: barNode, digest: barDigest},
   681  				// Redundant
   682  				"foo/bar/baz": {}, // content doesn't matter
   683  			},
   684  			wantItems: []*uploadItem{
   685  				uploadItemFromDirMsg("/", root),
   686  				uploadItemFromDirMsg("/foo", foo),
   687  			},
   688  		})
   689  	})
   690  
   691  	t.Run("nodes at different levels", func(t *testing.T) {
   692  		barDigest := digest.NewFromBlob([]byte("bar")).ToProto()
   693  		barNode := &repb.FileNode{
   694  			Name:   "bar",
   695  			Digest: barDigest,
   696  		}
   697  
   698  		bazDigest := digest.NewFromBlob([]byte("bar")).ToProto()
   699  		bazNode := &repb.FileNode{
   700  			Name:   "baz",
   701  			Digest: bazDigest,
   702  		}
   703  
   704  		foo := &repb.Directory{
   705  			Files: []*repb.FileNode{barNode},
   706  		}
   707  		root := &repb.Directory{
   708  			Directories: []*repb.DirectoryNode{{
   709  				Name:   "foo",
   710  				Digest: mustDigest(foo),
   711  			}},
   712  			Files: []*repb.FileNode{bazNode},
   713  		}
   714  
   715  		test(t, testCase{
   716  			tree: map[string]*digested{
   717  				"foo/bar": {dirEntry: barNode, digest: barDigest},
   718  				"baz":     {dirEntry: bazNode, digest: bazDigest}, // content doesn't matter
   719  			},
   720  			wantItems: []*uploadItem{
   721  				uploadItemFromDirMsg("/", root),
   722  				uploadItemFromDirMsg("/foo", foo),
   723  			},
   724  		})
   725  	})
   726  }
   727  
   728  func TestUploadInputInit(t *testing.T) {
   729  	t.Parallel()
   730  	absPath := filepath.Join(t.TempDir(), "foo")
   731  	testCases := []struct {
   732  		desc               string
   733  		in                 *UploadInput
   734  		dir                bool
   735  		wantCleanAllowlist []string
   736  		wantErrContain     string
   737  	}{
   738  		{
   739  			desc: "valid",
   740  			in:   &UploadInput{Path: absPath},
   741  		},
   742  		{
   743  			desc:           "relative path",
   744  			in:             &UploadInput{Path: "foo"},
   745  			wantErrContain: "not absolute",
   746  		},
   747  		{
   748  			desc:           "relative path",
   749  			in:             &UploadInput{Path: "foo"},
   750  			wantErrContain: "not absolute",
   751  		},
   752  		{
   753  			desc:           "regular file with allowlist",
   754  			in:             &UploadInput{Path: absPath, Allowlist: []string{"x"}},
   755  			wantErrContain: "the Allowlist is not supported for regular files",
   756  		},
   757  		{
   758  			desc:               "not clean allowlisted path",
   759  			in:                 &UploadInput{Path: absPath, Allowlist: []string{"bar/"}},
   760  			dir:                true,
   761  			wantCleanAllowlist: []string{"bar"},
   762  		},
   763  		{
   764  			desc:           "absolute allowlisted path",
   765  			in:             &UploadInput{Path: absPath, Allowlist: []string{"/bar"}},
   766  			dir:            true,
   767  			wantErrContain: "not relative",
   768  		},
   769  		{
   770  			desc:           "parent dir in allowlisted path",
   771  			in:             &UploadInput{Path: absPath, Allowlist: []string{"bar/../.."}},
   772  			dir:            true,
   773  			wantErrContain: "..",
   774  		},
   775  		{
   776  			desc:               "no allowlist",
   777  			in:                 &UploadInput{Path: absPath},
   778  			dir:                true,
   779  			wantCleanAllowlist: []string{"."},
   780  		},
   781  	}
   782  
   783  	for _, tc := range testCases {
   784  		tc := tc
   785  		t.Run(tc.desc, func(t *testing.T) {
   786  			tmpFilePath := absPath
   787  			if tc.dir {
   788  				tmpFilePath = filepath.Join(absPath, "bar")
   789  			}
   790  			putFile(t, tmpFilePath, "")
   791  			defer os.RemoveAll(absPath)
   792  
   793  			err := tc.in.init(&uploader{})
   794  			if tc.wantErrContain == "" {
   795  				if err != nil {
   796  					t.Error(err)
   797  				}
   798  			} else {
   799  				if err == nil || !strings.Contains(err.Error(), tc.wantErrContain) {
   800  					t.Errorf("expected err to contain %q; got %v", tc.wantErrContain, err)
   801  				}
   802  			}
   803  
   804  			if len(tc.wantCleanAllowlist) != 0 {
   805  				if diff := cmp.Diff(tc.wantCleanAllowlist, tc.in.cleanAllowlist); diff != "" {
   806  					t.Errorf("unexpected cleanAllowlist (-want +got):\n%s", diff)
   807  				}
   808  			}
   809  		})
   810  	}
   811  }
   812  
   813  func compareUploadItems(x, y *uploadItem) bool {
   814  	return x.Title == y.Title &&
   815  		proto.Equal(x.Digest, y.Digest) &&
   816  		((x.Open == nil && y.Open == nil) || cmp.Equal(mustReadAll(x), mustReadAll(y)))
   817  }
   818  
   819  func mustReadAll(item *uploadItem) []byte {
   820  	data, err := item.ReadAll()
   821  	if err != nil {
   822  		panic(err)
   823  	}
   824  	return data
   825  }
   826  
   827  func uploadInputChanFrom(inputs ...*UploadInput) chan *UploadInput {
   828  	ch := make(chan *UploadInput, len(inputs))
   829  	for _, in := range inputs {
   830  		ch <- in
   831  	}
   832  	close(ch)
   833  	return ch
   834  }
   835  
   836  type fakeCAS struct {
   837  	regrpc.ContentAddressableStorageClient
   838  	findMissingBlobs func(ctx context.Context, in *repb.FindMissingBlobsRequest, opts ...grpc.CallOption) (*repb.FindMissingBlobsResponse, error)
   839  	batchUpdateBlobs func(ctx context.Context, in *repb.BatchUpdateBlobsRequest, opts ...grpc.CallOption) (*repb.BatchUpdateBlobsResponse, error)
   840  }
   841  
   842  func (c *fakeCAS) FindMissingBlobs(ctx context.Context, in *repb.FindMissingBlobsRequest, opts ...grpc.CallOption) (*repb.FindMissingBlobsResponse, error) {
   843  	return c.findMissingBlobs(ctx, in, opts...)
   844  }
   845  
   846  func (c *fakeCAS) BatchUpdateBlobs(ctx context.Context, in *repb.BatchUpdateBlobsRequest, opts ...grpc.CallOption) (*repb.BatchUpdateBlobsResponse, error) {
   847  	return c.batchUpdateBlobs(ctx, in, opts...)
   848  }
   849  
   850  func putFile(t *testing.T, path, contents string) {
   851  	if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
   852  		t.Fatal(err)
   853  	}
   854  	if err := os.WriteFile(path, []byte(contents), 0600); err != nil {
   855  		t.Fatal(err)
   856  	}
   857  }
   858  
   859  func putSymlink(t *testing.T, path, target string) {
   860  	if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil {
   861  		t.Fatal(err)
   862  	}
   863  	if err := os.Symlink(target, path); err != nil {
   864  		t.Fatal(err)
   865  	}
   866  }