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  }