ea1408f7a8
As a reminder: latest clingy removed the requirement of having custom context (which made the usage of context.WithValue harder) and uses simple context instead. Clingy saves the stdin/stdout/stderr to the context (earlier to separated context type) to make it available for unit testing. Change-Id: I8896574f4670721de43a577cd4b35952e3b5d00e
241 lines
6.0 KiB
Go
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 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 := 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 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)
|
|
}}
|
|
}
|