storj/cmd/uplinkng/ultest/result.go
Jeff Wendling 08d860570b cmd/uplinkng: parallelsm and a ton of fixes
this was just supposed to add parallel uploads/downloads
and it does do that, but i then found a bunch of bugs
with respect to path handling that i thought i had under
control. oops.

so this adds a ton of tests and tries to make the logic
in ulloc to be more consistent. almost all of the actual
file handling bits and knowledge happens in cmd_cp now
where it should belong.

additionally, the s3 command has the behavior that if your
bucket has the file s3://bucket/file, then executing
s3 ls s3://bucket/fi returns nothing. this change makes
uplinkng match that behavior even if i don't personally
like it.

a big portion of the weirdness is the concept introduced
that i've named "directoryish", which intends to capture
the behavior that if a user copies a file to that location
then the base name of the source should be appended on
rather than a direct copy. this concept is entirely a
based on the string value and not the actual filesystem
state. hence, the cp command is responsible for checking
if local paths are actually a directory, and adding a
trailing slash if necessary to make them "directoryish".
additionally, the empty key for a bucket and the empty
string for local paths are considered "directoryish".

Change-Id: I9120d18616fd813b29ff81beed4f5993caa99fb6
2021-08-11 02:30:06 +00:00

148 lines
3.9 KiB
Go

// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package ultest
import (
"sort"
"strings"
"testing"
"github.com/stretchr/testify/require"
"storj.io/storj/cmd/uplinkng/ulloc"
)
// Result captures all the output of running a command for inspection.
type Result struct {
Stdout string
Stderr string
Ok bool
Err error
Files []File
}
// RequireSuccess fails if the Result did not observe a successful execution.
func (r Result) RequireSuccess(t *testing.T) {
if !r.Ok {
errs := parseErrors(r.Stdout)
require.FailNow(t, "test did not run successfully:",
"%s", strings.Join(errs, "\n"))
}
require.NoError(t, r.Err)
}
// RequireFailure fails if the Result did not observe a failed execution.
func (r Result) RequireFailure(t *testing.T) Result {
require.False(t, r.Ok && r.Err == nil, "command ran with no error")
return r
}
// RequireStdout requires that the execution wrote to stdout the provided string.
// Blank lines are ignored and all lines are space trimmed for the comparison.
func (r Result) RequireStdout(t *testing.T, stdout string) Result {
require.Equal(t, trimNewlineSpaces(stdout), trimNewlineSpaces(r.Stdout))
return r
}
// RequireStderr requires that the execution wrote to stderr the provided string.
// Blank lines are ignored and all lines are space trimmed for the comparison.
func (r Result) RequireStderr(t *testing.T, stderr string) Result {
require.Equal(t, trimNewlineSpaces(stderr), trimNewlineSpaces(r.Stderr))
return r
}
// RequireFiles requires that the set of files provided are all of the files that
// existed at the end of the execution. It assumes any passed in files with no
// contents contain the filename as the contents instead.
func (r Result) RequireFiles(t *testing.T, files ...File) Result {
require.Equal(t, canonicalizeFiles(files), r.Files)
return r
}
// RequireLocalFiles requires that the set of files provided are all of the
// local files that existed at the end of the execution. It assumes any passed
// in files with no contents contain the filename as the contents instead.
func (r Result) RequireLocalFiles(t *testing.T, files ...File) Result {
require.Equal(t, canonicalizeFiles(files), filterFiles(r.Files, fileIsLocal))
return r
}
// RequireRemoteFiles requires that the set of files provided are all of the
// remote files that existed at the end of the execution. It assumes any passed
// in files with no contents contain the filename as the contents instead.
func (r Result) RequireRemoteFiles(t *testing.T, files ...File) Result {
require.Equal(t, canonicalizeFiles(files), filterFiles(r.Files, fileIsRemote))
return r
}
func filterFiles(files []File, match func(File) bool) (out []File) {
for _, file := range files {
if match(file) {
out = append(out, file)
}
}
return out
}
func canonicalizeFiles(files []File) (out []File) {
out = append(out, files...)
sort.Slice(out, func(i, j int) bool { return out[i].less(out[j]) })
for i := range out {
if out[i].Contents == "" {
out[i].Contents = out[i].Loc
}
}
return out
}
func fileIsLocal(file File) bool {
loc, _ := ulloc.Parse(file.Loc)
return loc.Local()
}
func fileIsRemote(file File) bool {
loc, _ := ulloc.Parse(file.Loc)
return loc.Remote()
}
func parseErrors(s string) []string {
lines := strings.Split(s, "\n")
start := 0
for i, line := range lines {
if line == "Errors:" {
start = i + 1
} else if len(line) > 0 && line[0] != ' ' {
return lines[start:i]
}
}
return nil
}
func trimNewlineSpaces(s string) string {
lines := strings.Split(s, "\n")
j := 0
for _, line := range lines {
if trimmed := strings.TrimSpace(line); len(trimmed) > 0 {
lines[j] = trimmed
j++
}
}
return strings.Join(lines[:j], "\n")
}
// File represents a file existing either locally or remotely.
type File struct {
Loc string
Contents string
}
func (f File) less(g File) bool {
fl, _ := ulloc.Parse(f.Loc)
gl, _ := ulloc.Parse(g.Loc)
return fl.Less(gl)
}