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
18
19
20
21
22
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
131
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
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