go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/cmd/artifacts/copy_test.go (about) 1 // Copyright 2020 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 package main 5 6 import ( 7 "bytes" 8 "context" 9 "errors" 10 "fmt" 11 "os" 12 "path/filepath" 13 "strings" 14 "sync" 15 "testing" 16 17 "cloud.google.com/go/storage" 18 buildbucketpb "go.chromium.org/luci/buildbucket/proto" 19 "google.golang.org/protobuf/types/known/structpb" 20 21 "go.fuchsia.dev/infra/artifacts" 22 ) 23 24 type mockDirectory struct { 25 files []string 26 copiedFiles []string 27 lock sync.Mutex 28 } 29 30 func (d *mockDirectory) Object(path string) *storage.ObjectHandle { 31 return nil 32 } 33 34 func (d *mockDirectory) CopyFile(ctx context.Context, src, dest string) (int64, error) { 35 for _, file := range d.files { 36 if file == src { 37 d.lock.Lock() 38 d.copiedFiles = append(d.copiedFiles, dest) 39 d.lock.Unlock() 40 return 1, nil 41 } 42 } 43 return 0, os.ErrNotExist 44 } 45 46 func (d *mockDirectory) List(ctx context.Context, prefix string) ([]string, error) { 47 var result []string 48 for _, file := range d.files { 49 if strings.HasPrefix(file, prefix) { 50 result = append(result, file) 51 } 52 } 53 if len(result) == 0 { 54 return nil, &artifacts.ErrNothingMatchedPrefix{ 55 BucketName: "bucket", 56 Prefix: prefix, 57 } 58 } 59 return result, nil 60 } 61 62 type mockArtifactsClient struct { 63 files map[string][]string 64 dir *mockDirectory 65 } 66 67 func (a *mockArtifactsClient) GetBuildDir(bucket, buildID string) artifacts.Directory { 68 a.dir = &mockDirectory{ 69 files: a.files[bucket], 70 } 71 return a.dir 72 } 73 74 func createSrcsFile(srcsFileContents []string) (*os.File, error) { 75 f, err := os.CreateTemp("", "src-file") 76 if err != nil { 77 return nil, err 78 } 79 80 for _, src := range srcsFileContents { 81 if _, err := fmt.Fprintf(f, "%s\n", src); err != nil { 82 os.Remove(f.Name()) 83 return nil, err 84 } 85 } 86 87 return f, nil 88 } 89 90 func TestExecute(t *testing.T) { 91 archiveBucket := []string{ 92 "build-archive.tgz", 93 "packages.tar.gz", 94 } 95 artifactsBucket := []string{ 96 "images/a", 97 "images/b/c", 98 "images/c", 99 "packages/a", 100 "packages/b/c", 101 } 102 storageContents := map[string][]string{ 103 "gcs_bucket": archiveBucket, 104 "artifacts_bucket": artifactsBucket, 105 } 106 artifactsCli := &mockArtifactsClient{ 107 files: storageContents, 108 } 109 buildID := "123" 110 buildsCli := &mockBuildsClient{ 111 response: &buildbucketpb.Build{ 112 Id: 123, 113 Output: &buildbucketpb.Build_Output{ 114 Properties: &structpb.Struct{ 115 Fields: map[string]*structpb.Value{ 116 "gcs_bucket": { 117 Kind: &structpb.Value_StringValue{ 118 StringValue: "gcs_bucket", 119 }, 120 }, 121 "artifact_gcs_bucket": { 122 Kind: &structpb.Value_StringValue{ 123 StringValue: "artifacts_bucket", 124 }, 125 }, 126 }, 127 }, 128 }, 129 }, 130 } 131 tests := []struct { 132 name string 133 source string 134 dest string 135 srcsFileContents []string 136 expectedFiles []string 137 checkErr func(t *testing.T, err error, stdout string) 138 }{ 139 { 140 name: "copy directory from the artifacts bucket", 141 source: "packages", 142 dest: "output/packages", 143 expectedFiles: []string{"output/packages/a", "output/packages/b/c"}, 144 }, 145 { 146 name: "copy file", 147 source: "images/a", 148 dest: "output/dest", 149 expectedFiles: []string{"output/dest"}, 150 }, 151 { 152 name: "copy file from src-file", 153 source: "images/a", 154 dest: "output", 155 srcsFileContents: []string{"images/a"}, 156 expectedFiles: []string{"output"}, 157 }, 158 { 159 name: "copy multiple files from src-file", 160 source: "images", 161 dest: "output", 162 srcsFileContents: []string{"images/a", "images/c"}, 163 expectedFiles: []string{"output/a", "output/c"}, 164 }, 165 { 166 name: "copy from src-file with empty source", 167 dest: "output", 168 srcsFileContents: []string{"images/a", "images/c"}, 169 expectedFiles: []string{"output/images/a", "output/images/c"}, 170 }, 171 { 172 name: "copy src returns an error", 173 source: "does-not-exist", 174 dest: "output/packages", 175 checkErr: func(t *testing.T, err error, _ string) { 176 var e *artifacts.ErrNothingMatchedPrefix 177 if ok := errors.As(err, &e); !ok { 178 t.Errorf("error should be ErrNothingMatchedPrefix, not %T", err) 179 } 180 181 if e.BucketName != "bucket" { 182 t.Errorf("expected error bucket to be 'bucket', not %s", e.BucketName) 183 } 184 185 if e.Prefix != "does-not-exist" { 186 t.Errorf("expected error bucket to be 'does-not-exist', not %s", e.Prefix) 187 } 188 }, 189 }, 190 { 191 name: "copy src-files returns an error", 192 dest: "output", 193 srcsFileContents: []string{"does-not-exist", "images/a", "images/c"}, 194 checkErr: func(t *testing.T, err error, stdout string) { 195 if !os.IsNotExist(err) { 196 t.Errorf("expected error to be not found, not %s", err) 197 } 198 if !strings.Contains(stdout, "failed to download") { 199 t.Errorf("expected log:\nfailed to download\nbut got:\n%+v", stdout) 200 } 201 }, 202 }, 203 } 204 205 for _, tt := range tests { 206 t.Run(tt.name, func(t *testing.T) { 207 srcsFile := "" 208 if tt.srcsFileContents != nil { 209 f, err := createSrcsFile(tt.srcsFileContents) 210 if err != nil { 211 t.Errorf("failed to write src-file contents: %s", err) 212 } 213 defer os.Remove(f.Name()) 214 215 srcsFile = f.Name() 216 } 217 cmd := &CopyCommand{ 218 build: buildID, 219 source: tt.source, 220 dest: tt.dest, 221 srcsFile: srcsFile, 222 j: 1, 223 verbose: true, 224 } 225 testStdout := &bytes.Buffer{} 226 stdout = testStdout 227 err := cmd.execute(context.Background(), buildsCli, artifactsCli) 228 actualStdout := string(testStdout.Bytes()) 229 if tt.checkErr != nil { 230 tt.checkErr(t, err, actualStdout) 231 return 232 } else if err != nil { 233 t.Errorf("unexpected error: %s", err) 234 } 235 236 compare := func(expected []string, actual []string) { 237 if len(expected) != len(actual) { 238 t.Errorf("expected:\n%+v\nbut got:\n%+v", expected, actual) 239 } 240 fileMap := map[string]bool{} 241 for _, file := range expected { 242 fileMap[file] = true 243 } 244 for _, file := range actual { 245 if _, ok := fileMap[file]; !ok { 246 t.Errorf("expected:\n%+v\nbut got:\n%+v", expected, actual) 247 } 248 } 249 } 250 251 compare(tt.expectedFiles, artifactsCli.dir.copiedFiles) 252 253 var expectedLogs []string 254 for _, file := range tt.expectedFiles { 255 relpath, err := filepath.Rel(tt.dest, file) 256 if err != nil { 257 t.Fatal(err) 258 } 259 source := filepath.Join(tt.source, relpath) 260 expectedLogs = append(expectedLogs, fmt.Sprintf("%s (1 bytes) to %s", source, tt.dest)) 261 } 262 numFiles := len(tt.expectedFiles) 263 expectedLogs = append(expectedLogs, fmt.Sprintf("Num files: %d\nTotal bytes: %d", numFiles, numFiles)) 264 for _, log := range expectedLogs { 265 if !strings.Contains(actualStdout, log) { 266 t.Errorf("expected log:\n%+v\nbut got:\n%+v", log, actualStdout) 267 } 268 } 269 }) 270 } 271 }