...

Source file src/gotest.tools/v3/fs/report.go

Documentation: gotest.tools/v3/fs

     1  package fs
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"path/filepath"
     9  	"runtime"
    10  	"sort"
    11  	"strings"
    12  
    13  	"gotest.tools/v3/assert/cmp"
    14  	"gotest.tools/v3/internal/format"
    15  )
    16  
    17  // Equal compares a directory to the expected structured described by a manifest
    18  // and returns success if they match. If they do not match the failure message
    19  // will contain all the differences between the directory structure and the
    20  // expected structure defined by the [Manifest].
    21  //
    22  // Equal is a [cmp.Comparison] which can be used with [gotest.tools/v3/assert.Assert].
    23  func Equal(path string, expected Manifest) cmp.Comparison {
    24  	return func() cmp.Result {
    25  		actual, err := manifestFromDir(path)
    26  		if err != nil {
    27  			return cmp.ResultFromError(err)
    28  		}
    29  		failures := eqDirectory(string(os.PathSeparator), expected.root, actual.root)
    30  		if len(failures) == 0 {
    31  			return cmp.ResultSuccess
    32  		}
    33  		msg := fmt.Sprintf("directory %s does not match expected:\n", path)
    34  		return cmp.ResultFailure(msg + formatFailures(failures))
    35  	}
    36  }
    37  
    38  type failure struct {
    39  	path     string
    40  	problems []problem
    41  }
    42  
    43  type problem string
    44  
    45  func notEqual(property string, x, y interface{}) problem {
    46  	return problem(fmt.Sprintf("%s: expected %s got %s", property, x, y))
    47  }
    48  
    49  func errProblem(reason string, err error) problem {
    50  	return problem(fmt.Sprintf("%s: %s", reason, err))
    51  }
    52  
    53  func existenceProblem(filename string, msgAndArgs ...interface{}) problem {
    54  	return problem(filename + ": " + format.Message(msgAndArgs...))
    55  }
    56  
    57  func eqResource(x, y resource) []problem {
    58  	var p []problem
    59  	if x.uid != y.uid {
    60  		p = append(p, notEqual("uid", x.uid, y.uid))
    61  	}
    62  	if x.gid != y.gid {
    63  		p = append(p, notEqual("gid", x.gid, y.gid))
    64  	}
    65  	if x.mode != anyFileMode && x.mode != y.mode {
    66  		p = append(p, notEqual("mode", x.mode, y.mode))
    67  	}
    68  	return p
    69  }
    70  
    71  func removeCarriageReturn(in []byte) []byte {
    72  	return bytes.Replace(in, []byte("\r\n"), []byte("\n"), -1)
    73  }
    74  
    75  func eqFile(x, y *file) []problem {
    76  	p := eqResource(x.resource, y.resource)
    77  
    78  	switch {
    79  	case x.content == nil:
    80  		p = append(p, existenceProblem("content", "expected content is nil"))
    81  		return p
    82  	case x.content == anyFileContent:
    83  		return p
    84  	case y.content == nil:
    85  		p = append(p, existenceProblem("content", "actual content is nil"))
    86  		return p
    87  	}
    88  
    89  	xContent, xErr := io.ReadAll(x.content)
    90  	defer x.content.Close()
    91  	yContent, yErr := io.ReadAll(y.content)
    92  	defer y.content.Close()
    93  
    94  	if xErr != nil {
    95  		p = append(p, errProblem("failed to read expected content", xErr))
    96  	}
    97  	if yErr != nil {
    98  		p = append(p, errProblem("failed to read actual content", xErr))
    99  	}
   100  	if xErr != nil || yErr != nil {
   101  		return p
   102  	}
   103  
   104  	if x.compareContentFunc != nil {
   105  		r := x.compareContentFunc(yContent)
   106  		if !r.Success() {
   107  			p = append(p, existenceProblem("content", r.FailureMessage()))
   108  		}
   109  		return p
   110  	}
   111  
   112  	if x.ignoreCariageReturn || y.ignoreCariageReturn {
   113  		xContent = removeCarriageReturn(xContent)
   114  		yContent = removeCarriageReturn(yContent)
   115  	}
   116  
   117  	if !bytes.Equal(xContent, yContent) {
   118  		p = append(p, diffContent(xContent, yContent))
   119  	}
   120  	return p
   121  }
   122  
   123  func diffContent(x, y []byte) problem {
   124  	diff := format.UnifiedDiff(format.DiffConfig{
   125  		A:    string(x),
   126  		B:    string(y),
   127  		From: "expected",
   128  		To:   "actual",
   129  	})
   130  	// Remove the trailing newline in the diff. A trailing newline is always
   131  	// added to a problem by formatFailures.
   132  	diff = strings.TrimSuffix(diff, "\n")
   133  	return problem("content:\n" + indent(diff, "    "))
   134  }
   135  
   136  func indent(s, prefix string) string {
   137  	buf := new(bytes.Buffer)
   138  	lines := strings.SplitAfter(s, "\n")
   139  	for _, line := range lines {
   140  		buf.WriteString(prefix + line)
   141  	}
   142  	return buf.String()
   143  }
   144  
   145  func eqSymlink(x, y *symlink) []problem {
   146  	p := eqResource(x.resource, y.resource)
   147  	xTarget := x.target
   148  	yTarget := y.target
   149  	if runtime.GOOS == "windows" {
   150  		xTarget = strings.ToLower(xTarget)
   151  		yTarget = strings.ToLower(yTarget)
   152  	}
   153  	if xTarget != yTarget {
   154  		p = append(p, notEqual("target", x.target, y.target))
   155  	}
   156  	return p
   157  }
   158  
   159  func eqDirectory(path string, x, y *directory) []failure {
   160  	p := eqResource(x.resource, y.resource)
   161  	var f []failure
   162  	matchedFiles := make(map[string]bool)
   163  
   164  	for _, name := range sortedKeys(x.items) {
   165  		if name == anyFile {
   166  			continue
   167  		}
   168  		matchedFiles[name] = true
   169  		xEntry := x.items[name]
   170  		yEntry, ok := y.items[name]
   171  		if !ok {
   172  			p = append(p, existenceProblem(name, "expected %s to exist", xEntry.Type()))
   173  			continue
   174  		}
   175  
   176  		if xEntry.Type() != yEntry.Type() {
   177  			p = append(p, notEqual(name, xEntry.Type(), yEntry.Type()))
   178  			continue
   179  		}
   180  
   181  		f = append(f, eqEntry(filepath.Join(path, name), xEntry, yEntry)...)
   182  	}
   183  
   184  	if len(x.filepathGlobs) != 0 {
   185  		for _, name := range sortedKeys(y.items) {
   186  			m := matchGlob(name, y.items[name], x.filepathGlobs)
   187  			matchedFiles[name] = m.match
   188  			f = append(f, m.failures...)
   189  		}
   190  	}
   191  
   192  	if _, ok := x.items[anyFile]; ok {
   193  		return maybeAppendFailure(f, path, p)
   194  	}
   195  	for _, name := range sortedKeys(y.items) {
   196  		if !matchedFiles[name] {
   197  			p = append(p, existenceProblem(name, "unexpected %s", y.items[name].Type()))
   198  		}
   199  	}
   200  	return maybeAppendFailure(f, path, p)
   201  }
   202  
   203  func maybeAppendFailure(failures []failure, path string, problems []problem) []failure {
   204  	if len(problems) > 0 {
   205  		return append(failures, failure{path: path, problems: problems})
   206  	}
   207  	return failures
   208  }
   209  
   210  func sortedKeys(items map[string]dirEntry) []string {
   211  	keys := make([]string, 0, len(items))
   212  	for key := range items {
   213  		keys = append(keys, key)
   214  	}
   215  	sort.Strings(keys)
   216  	return keys
   217  }
   218  
   219  // eqEntry assumes x and y to be the same type
   220  func eqEntry(path string, x, y dirEntry) []failure {
   221  	resp := func(problems []problem) []failure {
   222  		if len(problems) == 0 {
   223  			return nil
   224  		}
   225  		return []failure{{path: path, problems: problems}}
   226  	}
   227  
   228  	switch typed := x.(type) {
   229  	case *file:
   230  		return resp(eqFile(typed, y.(*file)))
   231  	case *symlink:
   232  		return resp(eqSymlink(typed, y.(*symlink)))
   233  	case *directory:
   234  		return eqDirectory(path, typed, y.(*directory))
   235  	}
   236  	return nil
   237  }
   238  
   239  type globMatch struct {
   240  	match    bool
   241  	failures []failure
   242  }
   243  
   244  func matchGlob(name string, yEntry dirEntry, globs map[string]*filePath) globMatch {
   245  	m := globMatch{}
   246  
   247  	for glob, expectedFile := range globs {
   248  		ok, err := filepath.Match(glob, name)
   249  		if err != nil {
   250  			p := errProblem("failed to match glob pattern", err)
   251  			f := failure{path: name, problems: []problem{p}}
   252  			m.failures = append(m.failures, f)
   253  		}
   254  		if ok {
   255  			m.match = true
   256  			m.failures = eqEntry(name, expectedFile.file, yEntry)
   257  			return m
   258  		}
   259  	}
   260  	return m
   261  }
   262  
   263  func formatFailures(failures []failure) string {
   264  	sort.Slice(failures, func(i, j int) bool {
   265  		return failures[i].path < failures[j].path
   266  	})
   267  
   268  	buf := new(bytes.Buffer)
   269  	for _, failure := range failures {
   270  		buf.WriteString(failure.path + "\n")
   271  		for _, problem := range failure.problems {
   272  			buf.WriteString("  " + string(problem) + "\n")
   273  		}
   274  	}
   275  	return buf.String()
   276  }
   277  

View as plain text