storj/cmd/uplink/ultest/setup.go
Jeff Wendling 89ccfe2dd7 cmd/uplink: fix recursive copy and improve tests
recursive copy had a bug with relative local paths.

this fixes that bug and changes the test framework
to use more of the code that actually runs in uplink
and only mocks out the direct interaction with the
operating system.

Change-Id: I9da2a80bfda8f86a8d05879b87171f299f759c7e
2022-05-11 15:17:16 -04:00

241 lines
6.0 KiB
Go

// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package ultest
import (
"bytes"
"context"
"io/ioutil"
"sort"
"testing"
"github.com/stretchr/testify/require"
"github.com/zeebo/clingy"
"storj.io/storj/cmd/uplink/ulext"
"storj.io/storj/cmd/uplink/ulfs"
"storj.io/storj/cmd/uplink/ulloc"
)
// Commands is an alias to refer to a function that builds clingy commands.
type Commands = func(clingy.Commands, ulext.External)
// Setup returns some State that can be run multiple times with different command
// line arguments.
func Setup(cmds Commands, opts ...ExecuteOption) State {
return State{
cmds: cmds,
opts: opts,
}
}
// State represents some state and environment for a command to execute in.
type State struct {
cmds Commands
opts []ExecuteOption
}
// With appends the provided options and returns a new State.
func (st State) With(opts ...ExecuteOption) State {
st.opts = append([]ExecuteOption(nil), st.opts...)
st.opts = append(st.opts, opts...)
return st
}
// Succeed is the same as Run followed by result.RequireSuccess.
func (st State) Succeed(t *testing.T, args ...string) Result {
result := st.Run(t, args...)
result.RequireSuccess(t)
return result
}
// Fail is the same as Run followed by result.RequireFailure.
func (st State) Fail(t *testing.T, args ...string) Result {
result := st.Run(t, args...)
result.RequireFailure(t)
return result
}
// Run executes the command specified by the args and returns a Result.
func (st State) Run(t *testing.T, args ...string) Result {
var stdout bytes.Buffer
var stderr bytes.Buffer
var stdin bytes.Buffer
var ran bool
ctx := context.Background()
lfs := ulfs.NewLocal(ulfs.NewLocalBackendMem())
rfs := newRemoteFilesystem()
fs := ulfs.NewMixed(lfs, rfs)
cs := &callbackState{
fs: fs,
rfs: rfs,
}
ok, err := clingy.Environment{
Name: "uplink-test",
Args: args,
Stdin: &stdin,
Stdout: &stdout,
Stderr: &stderr,
Wrap: func(ctx clingy.Context, cmd clingy.Command) error {
for _, opt := range st.opts {
opt.fn(t, ctx, cs)
}
if len(cs.stdin) > 0 {
_, _ = stdin.WriteString(cs.stdin)
}
ran = true
return cmd.Execute(ctx)
},
}.Run(ctx, func(cmds clingy.Commands) {
st.cmds(cmds, newExternal(fs, nil))
})
if ok && err == nil {
require.True(t, ran, "no command was executed: %q", args)
}
files := rfs.Files()
files = gatherLocalFiles(ctx, t, lfs, files)
sort.Slice(files, func(i, j int) bool { return files[i].less(files[j]) })
return Result{
Stdout: stdout.String(),
Stderr: stderr.String(),
Ok: ok,
Err: err,
Files: files,
Pending: rfs.Pending(),
}
}
func gatherLocalFiles(ctx context.Context, t *testing.T, fs ulfs.FilesystemLocal, files []File) []File {
{
iter, err := fs.List(ctx, "", &ulfs.ListOptions{Recursive: true})
require.NoError(t, err)
files = collectIterator(ctx, t, fs, iter, files)
}
{
iter, err := fs.List(ctx, "/", &ulfs.ListOptions{Recursive: true})
require.NoError(t, err)
files = collectIterator(ctx, t, fs, iter, files)
}
return files
}
func collectIterator(ctx context.Context, t *testing.T, fs ulfs.FilesystemLocal, iter ulfs.ObjectIterator, files []File) []File {
for iter.Next() {
func() {
loc := iter.Item().Loc.Loc()
mrh, err := fs.Open(ctx, loc)
require.NoError(t, err)
defer func() { _ = mrh.Close() }()
rh, err := mrh.NextPart(ctx, -1)
require.NoError(t, err)
defer func() { _ = rh.Close() }()
data, err := ioutil.ReadAll(rh)
require.NoError(t, err)
files = append(files, File{
Loc: loc,
Contents: string(data),
})
}()
}
require.NoError(t, iter.Err())
return files
}
type callbackState struct {
stdin string
fs ulfs.Filesystem
rfs *remoteFilesystem
}
// ExecuteOption allows one to control the environment that a command executes in.
type ExecuteOption struct {
fn func(t *testing.T, ctx clingy.Context, cs *callbackState)
}
// WithFilesystem lets one do arbitrary setup on the filesystem in a callback.
func WithFilesystem(cb func(t *testing.T, ctx clingy.Context, fs ulfs.Filesystem)) ExecuteOption {
return ExecuteOption{func(t *testing.T, ctx clingy.Context, cs *callbackState) {
cb(t, ctx, cs.fs)
}}
}
// WithBucket ensures the bucket exists.
func WithBucket(name string) ExecuteOption {
return ExecuteOption{func(_ *testing.T, _ clingy.Context, cs *callbackState) {
cs.rfs.ensureBucket(name)
}}
}
// WithStdin sets the command to execute with the provided string as standard input.
func WithStdin(stdin string) ExecuteOption {
return ExecuteOption{func(_ *testing.T, _ clingy.Context, cs *callbackState) {
cs.stdin = stdin
}}
}
// WithFile sets the command to execute with a file created at the given location.
func WithFile(location string, contents ...string) ExecuteOption {
contents = append([]string(nil), contents...)
return ExecuteOption{func(t *testing.T, ctx clingy.Context, cs *callbackState) {
loc, err := ulloc.Parse(location)
require.NoError(t, err)
if bucket, _, ok := loc.RemoteParts(); ok {
cs.rfs.ensureBucket(bucket)
}
mwh, err := cs.fs.Create(ctx, loc, nil)
require.NoError(t, err)
defer func() { _ = mwh.Abort(ctx) }()
wh, err := mwh.NextPart(ctx, -1)
require.NoError(t, err)
defer func() { _ = wh.Abort() }()
for _, content := range contents {
_, err := wh.Write([]byte(content))
require.NoError(t, err)
}
if len(contents) == 0 {
_, err := wh.Write([]byte(location))
require.NoError(t, err)
}
require.NoError(t, wh.Commit())
require.NoError(t, mwh.Commit(ctx))
}}
}
// WithPendingFile sets the command to execute with a pending upload happening to
// the provided location.
func WithPendingFile(location string) ExecuteOption {
return ExecuteOption{func(t *testing.T, ctx clingy.Context, cs *callbackState) {
loc, err := ulloc.Parse(location)
require.NoError(t, err)
if bucket, _, ok := loc.RemoteParts(); ok {
cs.rfs.ensureBucket(bucket)
} else {
t.Fatalf("Invalid pending local file: %s", loc)
}
_, err = cs.fs.Create(ctx, loc, nil)
require.NoError(t, err)
}}
}