mirror of https://github.com/docker/cli.git
216 lines
5.1 KiB
Go
216 lines
5.1 KiB
Go
package fs
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"gotest.tools/assert/cmp"
|
|
"gotest.tools/internal/format"
|
|
)
|
|
|
|
// Equal compares a directory to the expected structured described by a manifest
|
|
// and returns success if they match. If they do not match the failure message
|
|
// will contain all the differences between the directory structure and the
|
|
// expected structure defined by the Manifest.
|
|
//
|
|
// Equal is a cmp.Comparison which can be used with assert.Assert().
|
|
func Equal(path string, expected Manifest) cmp.Comparison {
|
|
return func() cmp.Result {
|
|
actual, err := manifestFromDir(path)
|
|
if err != nil {
|
|
return cmp.ResultFromError(err)
|
|
}
|
|
failures := eqDirectory(string(os.PathSeparator), expected.root, actual.root)
|
|
if len(failures) == 0 {
|
|
return cmp.ResultSuccess
|
|
}
|
|
msg := fmt.Sprintf("directory %s does not match expected:\n", path)
|
|
return cmp.ResultFailure(msg + formatFailures(failures))
|
|
}
|
|
}
|
|
|
|
type failure struct {
|
|
path string
|
|
problems []problem
|
|
}
|
|
|
|
type problem string
|
|
|
|
func notEqual(property string, x, y interface{}) problem {
|
|
return problem(fmt.Sprintf("%s: expected %s got %s", property, x, y))
|
|
}
|
|
|
|
func errProblem(reason string, err error) problem {
|
|
return problem(fmt.Sprintf("%s: %s", reason, err))
|
|
}
|
|
|
|
func existenceProblem(filename, reason string, args ...interface{}) problem {
|
|
return problem(filename + ": " + fmt.Sprintf(reason, args...))
|
|
}
|
|
|
|
func eqResource(x, y resource) []problem {
|
|
var p []problem
|
|
if x.uid != y.uid {
|
|
p = append(p, notEqual("uid", x.uid, y.uid))
|
|
}
|
|
if x.gid != y.gid {
|
|
p = append(p, notEqual("gid", x.gid, y.gid))
|
|
}
|
|
if x.mode != anyFileMode && x.mode != y.mode {
|
|
p = append(p, notEqual("mode", x.mode, y.mode))
|
|
}
|
|
return p
|
|
}
|
|
|
|
func eqFile(x, y *file) []problem {
|
|
p := eqResource(x.resource, y.resource)
|
|
|
|
switch {
|
|
case x.content == nil:
|
|
p = append(p, existenceProblem("content", "expected content is nil"))
|
|
return p
|
|
case x.content == anyFileContent:
|
|
return p
|
|
case y.content == nil:
|
|
p = append(p, existenceProblem("content", "actual content is nil"))
|
|
return p
|
|
}
|
|
|
|
xContent, xErr := ioutil.ReadAll(x.content)
|
|
defer x.content.Close()
|
|
yContent, yErr := ioutil.ReadAll(y.content)
|
|
defer y.content.Close()
|
|
|
|
if xErr != nil {
|
|
p = append(p, errProblem("failed to read expected content", xErr))
|
|
}
|
|
if yErr != nil {
|
|
p = append(p, errProblem("failed to read actual content", xErr))
|
|
}
|
|
if xErr != nil || yErr != nil {
|
|
return p
|
|
}
|
|
|
|
if !bytes.Equal(xContent, yContent) {
|
|
p = append(p, diffContent(xContent, yContent))
|
|
}
|
|
return p
|
|
}
|
|
|
|
func diffContent(x, y []byte) problem {
|
|
diff := format.UnifiedDiff(format.DiffConfig{
|
|
A: string(x),
|
|
B: string(y),
|
|
From: "expected",
|
|
To: "actual",
|
|
})
|
|
// Remove the trailing newline in the diff. A trailing newline is always
|
|
// added to a problem by formatFailures.
|
|
diff = strings.TrimSuffix(diff, "\n")
|
|
return problem("content:\n" + indent(diff, " "))
|
|
}
|
|
|
|
func indent(s, prefix string) string {
|
|
buf := new(bytes.Buffer)
|
|
lines := strings.SplitAfter(s, "\n")
|
|
for _, line := range lines {
|
|
buf.WriteString(prefix + line)
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
func eqSymlink(x, y *symlink) []problem {
|
|
p := eqResource(x.resource, y.resource)
|
|
if x.target != y.target {
|
|
p = append(p, notEqual("target", x.target, y.target))
|
|
}
|
|
return p
|
|
}
|
|
|
|
func eqDirectory(path string, x, y *directory) []failure {
|
|
p := eqResource(x.resource, y.resource)
|
|
var f []failure
|
|
|
|
for _, name := range sortedKeys(x.items) {
|
|
if name == anyFile {
|
|
continue
|
|
}
|
|
xEntry := x.items[name]
|
|
yEntry, ok := y.items[name]
|
|
if !ok {
|
|
p = append(p, existenceProblem(name, "expected %s to exist", xEntry.Type()))
|
|
continue
|
|
}
|
|
|
|
if xEntry.Type() != yEntry.Type() {
|
|
p = append(p, notEqual(name, xEntry.Type(), yEntry.Type()))
|
|
continue
|
|
}
|
|
|
|
f = append(f, eqEntry(filepath.Join(path, name), xEntry, yEntry)...)
|
|
}
|
|
|
|
if _, ok := x.items[anyFile]; !ok {
|
|
for _, name := range sortedKeys(y.items) {
|
|
if _, ok := x.items[name]; !ok {
|
|
yEntry := y.items[name]
|
|
p = append(p, existenceProblem(name, "unexpected %s", yEntry.Type()))
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(p) > 0 {
|
|
f = append(f, failure{path: path, problems: p})
|
|
}
|
|
return f
|
|
}
|
|
|
|
func sortedKeys(items map[string]dirEntry) []string {
|
|
var keys []string
|
|
for key := range items {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
// eqEntry assumes x and y to be the same type
|
|
func eqEntry(path string, x, y dirEntry) []failure {
|
|
resp := func(problems []problem) []failure {
|
|
if len(problems) == 0 {
|
|
return nil
|
|
}
|
|
return []failure{{path: path, problems: problems}}
|
|
}
|
|
|
|
switch typed := x.(type) {
|
|
case *file:
|
|
return resp(eqFile(typed, y.(*file)))
|
|
case *symlink:
|
|
return resp(eqSymlink(typed, y.(*symlink)))
|
|
case *directory:
|
|
return eqDirectory(path, typed, y.(*directory))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func formatFailures(failures []failure) string {
|
|
sort.Slice(failures, func(i, j int) bool {
|
|
return failures[i].path < failures[j].path
|
|
})
|
|
|
|
buf := new(bytes.Buffer)
|
|
for _, failure := range failures {
|
|
buf.WriteString(failure.path + "\n")
|
|
for _, problem := range failure.problems {
|
|
buf.WriteString(" " + string(problem) + "\n")
|
|
}
|
|
}
|
|
return buf.String()
|
|
}
|