storj/cmd/uplink/ultest/setup.go

241 lines
6.0 KiB
Go
Raw Normal View History

// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package ultest
import (
"bytes"
"context"
"io"
"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 context.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 := io.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 context.Context, cs *callbackState)
}
// WithFilesystem lets one do arbitrary setup on the filesystem in a callback.
func WithFilesystem(cb func(t *testing.T, ctx context.Context, fs ulfs.Filesystem)) ExecuteOption {
return ExecuteOption{func(t *testing.T, ctx context.Context, cs *callbackState) {
cb(t, ctx, cs.fs)
}}
}
// WithBucket ensures the bucket exists.
func WithBucket(name string) ExecuteOption {
return ExecuteOption{func(_ *testing.T, _ context.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, _ context.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 context.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 context.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)
}}
}