forked from toolshed/abra
		
	We were running behind and there were quite some deprecations to update. This was mostly in the upstream copy/pasta package but seems quite minimal.
		
			
				
	
	
		
			277 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			277 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package fs
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"runtime"
 | |
| 	"sort"
 | |
| 	"strings"
 | |
| 
 | |
| 	"gotest.tools/v3/assert/cmp"
 | |
| 	"gotest.tools/v3/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 [gotest.tools/v3/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 string, msgAndArgs ...interface{}) problem {
 | |
| 	return problem(filename + ": " + format.Message(msgAndArgs...))
 | |
| }
 | |
| 
 | |
| 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 removeCarriageReturn(in []byte) []byte {
 | |
| 	return bytes.Replace(in, []byte("\r\n"), []byte("\n"), -1)
 | |
| }
 | |
| 
 | |
| 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 := io.ReadAll(x.content)
 | |
| 	defer x.content.Close()
 | |
| 	yContent, yErr := io.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 x.compareContentFunc != nil {
 | |
| 		r := x.compareContentFunc(yContent)
 | |
| 		if !r.Success() {
 | |
| 			p = append(p, existenceProblem("content", r.FailureMessage()))
 | |
| 		}
 | |
| 		return p
 | |
| 	}
 | |
| 
 | |
| 	if x.ignoreCariageReturn || y.ignoreCariageReturn {
 | |
| 		xContent = removeCarriageReturn(xContent)
 | |
| 		yContent = removeCarriageReturn(yContent)
 | |
| 	}
 | |
| 
 | |
| 	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)
 | |
| 	xTarget := x.target
 | |
| 	yTarget := y.target
 | |
| 	if runtime.GOOS == "windows" {
 | |
| 		xTarget = strings.ToLower(xTarget)
 | |
| 		yTarget = strings.ToLower(yTarget)
 | |
| 	}
 | |
| 	if xTarget != yTarget {
 | |
| 		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
 | |
| 	matchedFiles := make(map[string]bool)
 | |
| 
 | |
| 	for _, name := range sortedKeys(x.items) {
 | |
| 		if name == anyFile {
 | |
| 			continue
 | |
| 		}
 | |
| 		matchedFiles[name] = true
 | |
| 		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 len(x.filepathGlobs) != 0 {
 | |
| 		for _, name := range sortedKeys(y.items) {
 | |
| 			m := matchGlob(name, y.items[name], x.filepathGlobs)
 | |
| 			matchedFiles[name] = m.match
 | |
| 			f = append(f, m.failures...)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if _, ok := x.items[anyFile]; ok {
 | |
| 		return maybeAppendFailure(f, path, p)
 | |
| 	}
 | |
| 	for _, name := range sortedKeys(y.items) {
 | |
| 		if !matchedFiles[name] {
 | |
| 			p = append(p, existenceProblem(name, "unexpected %s", y.items[name].Type()))
 | |
| 		}
 | |
| 	}
 | |
| 	return maybeAppendFailure(f, path, p)
 | |
| }
 | |
| 
 | |
| func maybeAppendFailure(failures []failure, path string, problems []problem) []failure {
 | |
| 	if len(problems) > 0 {
 | |
| 		return append(failures, failure{path: path, problems: problems})
 | |
| 	}
 | |
| 	return failures
 | |
| }
 | |
| 
 | |
| func sortedKeys(items map[string]dirEntry) []string {
 | |
| 	keys := make([]string, 0, len(items))
 | |
| 	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
 | |
| }
 | |
| 
 | |
| type globMatch struct {
 | |
| 	match    bool
 | |
| 	failures []failure
 | |
| }
 | |
| 
 | |
| func matchGlob(name string, yEntry dirEntry, globs map[string]*filePath) globMatch {
 | |
| 	m := globMatch{}
 | |
| 
 | |
| 	for glob, expectedFile := range globs {
 | |
| 		ok, err := filepath.Match(glob, name)
 | |
| 		if err != nil {
 | |
| 			p := errProblem("failed to match glob pattern", err)
 | |
| 			f := failure{path: name, problems: []problem{p}}
 | |
| 			m.failures = append(m.failures, f)
 | |
| 		}
 | |
| 		if ok {
 | |
| 			m.match = true
 | |
| 			m.failures = eqEntry(name, expectedFile.file, yEntry)
 | |
| 			return m
 | |
| 		}
 | |
| 	}
 | |
| 	return m
 | |
| }
 | |
| 
 | |
| 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()
 | |
| }
 |