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 }