go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/cmd/gcs-util/up_test.go (about)

     1  // Copyright 2022 The Fuchsia Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style license that can be
     3  // found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"errors"
    11  	"io"
    12  	"os"
    13  	"path"
    14  	"path/filepath"
    15  	"reflect"
    16  	"strconv"
    17  	"sync"
    18  	"testing"
    19  	"time"
    20  
    21  	"cloud.google.com/go/storage"
    22  	"google.golang.org/api/googleapi"
    23  
    24  	errorutil "go.chromium.org/luci/common/errors"
    25  	"go.chromium.org/luci/common/retry/transient"
    26  	"go.fuchsia.dev/infra/cmd/gcs-util/types"
    27  )
    28  
    29  const (
    30  	maxFileSize = 1024
    31  )
    32  
    33  // A simple in-memory implementation of dataSink.
    34  type memSink struct {
    35  	contents map[string][]byte
    36  	mutex    sync.RWMutex
    37  	err      error
    38  }
    39  
    40  func newMemSink() *memSink {
    41  	return &memSink{
    42  		contents: make(map[string][]byte),
    43  	}
    44  }
    45  
    46  func (s *memSink) objectExistsAt(ctx context.Context, name string) (bool, *storage.ObjectAttrs, error) {
    47  	s.mutex.RLock()
    48  	defer s.mutex.RUnlock()
    49  	if s.err != nil {
    50  		return false, nil, s.err
    51  	}
    52  	if _, ok := s.contents[name]; !ok {
    53  		return false, nil, nil
    54  	}
    55  	attrs := &storage.ObjectAttrs{CustomTime: time.Now().AddDate(0, 0, -(daysSinceCustomTime + 1))}
    56  	return true, attrs, nil
    57  }
    58  
    59  func (s *memSink) write(ctx context.Context, upload *types.Upload) error {
    60  	s.mutex.Lock()
    61  	defer s.mutex.Unlock()
    62  	var reader io.Reader
    63  	if upload.Source != "" {
    64  		f, err := os.Open(upload.Source)
    65  		if err != nil {
    66  			return err
    67  		}
    68  		defer f.Close()
    69  		reader = f
    70  	} else {
    71  		reader = bytes.NewBuffer(upload.Contents)
    72  	}
    73  	content, err := io.ReadAll(reader)
    74  	if err != nil {
    75  		return err
    76  	}
    77  	s.contents[upload.Destination] = content
    78  	return nil
    79  }
    80  
    81  func sinkHasContents(t *testing.T, s *memSink, contents map[string][]byte) {
    82  	if len(s.contents) > len(contents) {
    83  		t.Fatalf("the sink has more contents than expected")
    84  	} else if len(s.contents) < len(contents) {
    85  		t.Fatal("the sink has less contents than expected")
    86  	}
    87  
    88  	for name, actual := range s.contents {
    89  		expected, ok := contents[name]
    90  		if !ok {
    91  			t.Fatalf("found unexpected content %q with name %q in sink", actual, name)
    92  		} else if string(expected) != string(actual) {
    93  			t.Fatalf("unexpected content with name %q: %q != %q", name, actual, expected)
    94  		}
    95  	}
    96  }
    97  
    98  // Given a mapping of relative filepath to byte contents, this utility populates
    99  // a temporary directory with those contents at those paths.
   100  func newDirWithContents(t *testing.T, contents map[string][]byte) string {
   101  	dir := t.TempDir()
   102  	for name, content := range contents {
   103  		p := filepath.Join(dir, name)
   104  		if err := os.MkdirAll(path.Dir(p), 0o700); err != nil {
   105  			t.Fatal(err)
   106  		}
   107  		if err := os.WriteFile(p, content, 0o400); err != nil {
   108  			t.Fatalf("failed to write %q at %q: %v", content, p, err)
   109  		}
   110  	}
   111  	return dir
   112  }
   113  
   114  func sinkHasExpectedContents(t *testing.T, actual, expected map[string][]byte, j int) {
   115  	dir := newDirWithContents(t, actual)
   116  	sink := newMemSink()
   117  	ctx := context.Background()
   118  	files := getUploadFiles(dir, expected)
   119  	if err := uploadFiles(ctx, files, sink, j, ""); err != nil {
   120  		t.Fatalf("failed to upload contents: %v", err)
   121  	}
   122  	sinkHasContents(t, sink, expected)
   123  }
   124  
   125  func getUploadFiles(dir string, fileContents map[string][]byte) []types.Upload {
   126  	var files []types.Upload
   127  	for f := range fileContents {
   128  		files = append(files, types.Upload{
   129  			Source:      filepath.Join(dir, f),
   130  			Destination: f,
   131  		})
   132  	}
   133  	return files
   134  }
   135  
   136  func TestUploading(t *testing.T) {
   137  	t.Run("uploads specific files", func(t *testing.T) {
   138  		actual := map[string][]byte{
   139  			"a":       []byte("one"),
   140  			"b":       []byte("two"),
   141  			"c/d":     []byte("three"),
   142  			"c/e/f/g": []byte("four"),
   143  		}
   144  		expected := map[string][]byte{
   145  			"a":   []byte("one"),
   146  			"c/d": []byte("three"),
   147  		}
   148  		sinkHasExpectedContents(t, actual, expected, 1)
   149  	})
   150  
   151  	t.Run("behaves under high concurrency", func(t *testing.T) {
   152  		actual := make(map[string][]byte)
   153  		for i := range 1000 {
   154  			s := strconv.Itoa(i)
   155  			actual[s] = []byte(s)
   156  		}
   157  		expected := actual
   158  		sinkHasExpectedContents(t, actual, expected, 100)
   159  	})
   160  
   161  	t.Run("only top-level files are uploaded", func(t *testing.T) {
   162  		actual := map[string][]byte{
   163  			"a":       []byte("one"),
   164  			"b":       []byte("two"),
   165  			"c/d":     []byte("three"),
   166  			"c/e/f/g": []byte("four"),
   167  		}
   168  		dir := types.Upload{Source: newDirWithContents(t, actual)}
   169  		expected := []types.Upload{
   170  			{Source: filepath.Join(dir.Source, "a"), Destination: "a"},
   171  			{Source: filepath.Join(dir.Source, "b"), Destination: "b"},
   172  		}
   173  		files, err := dirToFiles(context.Background(), dir)
   174  		if err != nil {
   175  			t.Fatalf("failed to read dir: %v", err)
   176  		}
   177  		if !reflect.DeepEqual(files, expected) {
   178  			t.Fatalf("unexpected files from dir: actual: %v, expected: %v", files, expected)
   179  		}
   180  	})
   181  
   182  	t.Run("all files are uploaded", func(t *testing.T) {
   183  		actual := map[string][]byte{
   184  			"a":       []byte("one"),
   185  			"b":       []byte("two"),
   186  			"c/d":     []byte("three"),
   187  			"c/e/f/g": []byte("four"),
   188  		}
   189  		dir := types.Upload{Source: newDirWithContents(t, actual), Recursive: true}
   190  		expected := []types.Upload{
   191  			{Source: filepath.Join(dir.Source, "a"), Destination: "a"},
   192  			{Source: filepath.Join(dir.Source, "b"), Destination: "b"},
   193  			{Source: filepath.Join(dir.Source, "c/d"), Destination: "c/d"},
   194  			{Source: filepath.Join(dir.Source, "c/e/f/g"), Destination: "c/e/f/g"},
   195  		}
   196  		files, err := dirToFiles(context.Background(), dir)
   197  		if err != nil {
   198  			t.Fatalf("failed to read dir: %v", err)
   199  		}
   200  		if !reflect.DeepEqual(files, expected) {
   201  			t.Fatalf("unexpected files from dir: actual: %v, expected: %v", files, expected)
   202  		}
   203  	})
   204  
   205  	t.Run("deduped objects are uploaded in objs_to_upload.txt", func(t *testing.T) {
   206  		actual := map[string][]byte{
   207  			"a":       []byte("one"),
   208  			"b":       []byte("two"),
   209  			"c/d":     []byte("three"),
   210  			"c/e/f/g": []byte("four"),
   211  		}
   212  		dir := types.Upload{Source: newDirWithContents(t, actual), Recursive: true, Deduplicate: true}
   213  		files, err := dirToFiles(context.Background(), dir)
   214  		if err != nil {
   215  			t.Fatalf("failed to read dir: %v", err)
   216  		}
   217  
   218  		sink := newMemSink()
   219  		sink.contents["b"] = []byte("two")
   220  		sink.contents["c/d"] = []byte("three")
   221  		expected := map[string][]byte{
   222  			"a":                 []byte("one"),
   223  			"b":                 []byte("two"),
   224  			"c/d":               []byte("three"),
   225  			"c/e/f/g":           []byte("four"),
   226  			objsToRefreshTTLTxt: []byte("b\nc/d"),
   227  		}
   228  
   229  		ctx := context.Background()
   230  		if err := uploadFiles(ctx, files, sink, 1, ""); err != nil {
   231  			t.Fatal(err)
   232  		}
   233  		sinkHasContents(t, sink, expected)
   234  	})
   235  
   236  	t.Run("transient errors identified", func(t *testing.T) {
   237  		// False for a regular error.
   238  		if isTransientError(errors.New("foo")) {
   239  			t.Fatal("non-transient error: got true, want false")
   240  		}
   241  		// False on HTTP response code 200.
   242  		gErr := new(googleapi.Error)
   243  		gErr.Code = 200
   244  		if isTransientError(gErr) {
   245  			t.Fatal("non-transient error: got true, want false")
   246  		}
   247  		// True for transient errors.
   248  		if !isTransientError(errorutil.New("a transient error", transient.Tag)) {
   249  			t.Fatal("explicit transient error: got false, want true")
   250  		}
   251  		// True on HTTP response code 500.
   252  		gErr.Code = 500
   253  		if !isTransientError(gErr) {
   254  			t.Fatal("googleapi transient error: got false, want true")
   255  		}
   256  
   257  		actual := map[string][]byte{
   258  			"a":       []byte("one"),
   259  			"b":       []byte("two"),
   260  			"c/d":     []byte("three"),
   261  			"c/e/f/g": []byte("four"),
   262  		}
   263  		expected := map[string][]byte{
   264  			"a":   []byte("one"),
   265  			"c/d": []byte("three"),
   266  		}
   267  
   268  		// Confirm no error on valid upload.
   269  		dir := newDirWithContents(t, actual)
   270  		sink := newMemSink()
   271  		ctx := context.Background()
   272  		files := getUploadFiles(dir, expected)
   273  		err := uploadFiles(ctx, files, sink, 1, "")
   274  		if isTransientError(err) {
   275  			t.Fatal("transient upload error: got true, want false")
   276  		}
   277  		// Check a non-transient error.
   278  		sink.err = errors.New("foo")
   279  		err = uploadFiles(ctx, files, sink, 1, "")
   280  		if isTransientError(err) {
   281  			t.Fatal("transient upload error: got true, want false")
   282  		}
   283  		// Now use a transient error.
   284  		sink.err = errorutil.New("a transient error", transient.Tag)
   285  		err = uploadFiles(ctx, files, sink, 1, "")
   286  		if !isTransientError(err) {
   287  			t.Fatal("transient upload error: got false, want true")
   288  		}
   289  	})
   290  }