blob: 97e96ab28bb888fb1b9d233b10303a713c6ee89e [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"bytes"
"context"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"cloud.google.com/go/storage"
_struct "github.com/golang/protobuf/ptypes/struct"
buildbucketpb "go.chromium.org/luci/buildbucket/proto"
"go.fuchsia.dev/infra/artifacts"
)
type mockDirectory struct {
files []string
copiedFiles []string
lock sync.Mutex
}
func (d *mockDirectory) Object(path string) *storage.ObjectHandle {
return nil
}
func (d *mockDirectory) CopyFile(ctx context.Context, src, dest string) (int64, error) {
for _, file := range d.files {
if file == src {
d.lock.Lock()
d.copiedFiles = append(d.copiedFiles, dest)
d.lock.Unlock()
return 1, nil
}
}
return 0, os.ErrNotExist
}
func (d *mockDirectory) List(ctx context.Context, prefix string) ([]string, error) {
var result []string
for _, file := range d.files {
if strings.HasPrefix(file, prefix) {
result = append(result, file)
}
}
if len(result) == 0 {
return nil, &artifacts.ErrNothingMatchedPrefix{
BucketName: "bucket",
Prefix: prefix,
}
}
return result, nil
}
type mockArtifactsClient struct {
files map[string][]string
dir *mockDirectory
}
func (a *mockArtifactsClient) GetBuildDir(bucket, buildID string) artifacts.Directory {
a.dir = &mockDirectory{
files: a.files[bucket],
}
return a.dir
}
func createSrcsFile(srcsFileContents []string) (*os.File, error) {
f, err := ioutil.TempFile("", "src-file")
if err != nil {
return nil, err
}
for _, src := range srcsFileContents {
if _, err := fmt.Fprintf(f, "%s\n", src); err != nil {
os.Remove(f.Name())
return nil, err
}
}
return f, nil
}
func TestExecute(t *testing.T) {
archiveBucket := []string{
"build-archive.tgz",
"packages.tar.gz",
}
artifactsBucket := []string{
"images/a",
"images/b/c",
"images/c",
"packages/a",
"packages/b/c",
}
storageContents := map[string][]string{
"gcs_bucket": archiveBucket,
"artifacts_bucket": artifactsBucket,
}
artifactsCli := &mockArtifactsClient{
files: storageContents,
}
buildID := "123"
buildsCli := &mockBuildsClient{
response: &buildbucketpb.Build{
Id: 123,
Output: &buildbucketpb.Build_Output{
Properties: &_struct.Struct{
Fields: map[string]*_struct.Value{
"gcs_bucket": {
Kind: &_struct.Value_StringValue{
StringValue: "gcs_bucket",
},
},
"artifact_gcs_bucket": {
Kind: &_struct.Value_StringValue{
StringValue: "artifacts_bucket",
},
},
},
},
},
},
}
tests := []struct {
name string
source string
dest string
srcsFileContents []string
expectedFiles []string
checkErr func(t *testing.T, err error, stdout string)
}{
{
name: "copy directory from the artifacts bucket",
source: "packages",
dest: "output/packages",
expectedFiles: []string{"output/packages/a", "output/packages/b/c"},
},
{
name: "copy archive",
source: "build-archive.tgz",
dest: "output/build-archive.tgz",
expectedFiles: []string{"output/build-archive.tgz"},
},
{
name: "copy from src-file",
source: "images",
dest: "output",
srcsFileContents: []string{"images/a", "images/c"},
expectedFiles: []string{"output/a", "output/c"},
},
{
name: "copy from src-file with empty source",
dest: "output",
srcsFileContents: []string{"images/a", "images/c"},
expectedFiles: []string{"output/images/a", "output/images/c"},
},
{
name: "copy src returns an error",
source: "does-not-exist",
dest: "output/packages",
checkErr: func(t *testing.T, err error, _ string) {
var e *artifacts.ErrNothingMatchedPrefix
if ok := errors.As(err, &e); !ok {
t.Errorf("error should be ErrNothingMatchedPrefix, not %T", err)
}
if e.BucketName != "bucket" {
t.Errorf("expected error bucket to be 'bucket', not %v", e.BucketName)
}
if e.Prefix != "does-not-exist" {
t.Errorf("expected error bucket to be 'does-not-exist', not %v", e.Prefix)
}
},
},
{
name: "copy src-files returns an error",
dest: "output",
srcsFileContents: []string{"does-not-exist", "images/a", "images/c"},
checkErr: func(t *testing.T, err error, stdout string) {
if !os.IsNotExist(err) {
t.Errorf("expected error to be not found, not %v", err)
}
if !strings.Contains(stdout, "failed to download") {
t.Errorf("expected log:\nfailed to download\nbut got:\n%+v", stdout)
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
srcsFile := ""
if tt.srcsFileContents != nil {
f, err := createSrcsFile(tt.srcsFileContents)
if err != nil {
t.Errorf("failed to write src-file contents: %v", err)
}
defer os.Remove(f.Name())
srcsFile = f.Name()
}
cmd := &CopyCommand{
build: buildID,
source: tt.source,
dest: tt.dest,
srcsFile: srcsFile,
j: 1,
}
testStdout := &bytes.Buffer{}
stdout = testStdout
err := cmd.execute(context.Background(), buildsCli, artifactsCli)
actualStdout := string(testStdout.Bytes())
if tt.checkErr != nil {
tt.checkErr(t, err, actualStdout)
return
} else if err != nil {
t.Errorf("unexpected error: %v", err)
}
compare := func(expected []string, actual []string) {
if len(expected) != len(actual) {
t.Errorf("expected:\n%+v\nbut got:\n%+v", expected, actual)
}
fileMap := map[string]bool{}
for _, file := range expected {
fileMap[file] = true
}
for _, file := range actual {
if _, ok := fileMap[file]; !ok {
t.Errorf("expected:\n%+v\nbut got:\n%+v", expected, actual)
}
}
}
compare(tt.expectedFiles, artifactsCli.dir.copiedFiles)
var expectedLogs []string
for _, file := range tt.expectedFiles {
relpath, err := filepath.Rel(tt.dest, file)
if err != nil {
t.Fatal(err)
}
source := filepath.Join(tt.source, relpath)
expectedLogs = append(expectedLogs, fmt.Sprintf("%s (1 bytes) to %s", source, tt.dest))
}
numFiles := len(tt.expectedFiles)
expectedLogs = append(expectedLogs, fmt.Sprintf("Num files: %d\nTotal bytes: %d", numFiles, numFiles))
for _, log := range expectedLogs {
if !strings.Contains(actualStdout, log) {
t.Errorf("expected log:\n%+v\nbut got:\n%+v", log, actualStdout)
}
}
})
}
}