image-automation-controller/internal/testutil/files.go

177 lines
4.6 KiB
Go

/*
Copyright 2020 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package testutil
import (
"os"
"path/filepath"
"strings"
. "github.com/onsi/gomega"
)
// ExpectMatchingDirectories compares two directories, recursively, and
// expects them to match. It will fail if there are files that are
// present in one directory but not the other, or if files that are
// present in both directories have different contents.
func ExpectMatchingDirectories(g *WithT, actualRoot, expectedRoot string) {
g.Expect(actualRoot).To(BeADirectory())
g.Expect(expectedRoot).To(BeADirectory())
actualonly, expectedonly, different := DiffDirectories(actualRoot, expectedRoot)
g.Expect(actualonly).To(BeEmpty(), "Expect no files in %s but not in %s", actualRoot, expectedRoot)
g.Expect(expectedonly).To(BeEmpty(), "Expect no files in %s but not in %s", expectedRoot, actualRoot)
// these are enumerated, so that the output is the actual difference
for _, diff := range different {
diff.FailedExpectation(g)
}
}
type Diff interface {
Path() string
FailedExpectation(g *WithT)
}
type contentDiff struct {
path, actual, expected string
}
func (d contentDiff) Path() string {
return d.path
}
func (d contentDiff) FailedExpectation(g *WithT) {
g.Expect(d.actual).To(Equal(d.expected))
}
type dirFile struct {
abspath, path string
expectedRegularFile bool
}
func (d dirFile) Path() string {
return d.path
}
func (d dirFile) FailedExpectation(g *WithT) {
if d.expectedRegularFile {
g.Expect(d.path).To(BeARegularFile())
} else {
g.Expect(d.path).To(BeADirectory())
}
}
// DiffDirectories walks the two given directories, recursively, and
// reports relative paths for any files that are:
//
// (in actual but not expected, in expected but not actual, in both but different)
//
// It ignores dot directories (e.g., `.git/`) and Emacs backups (e.g.,
// `foo.yaml~`). It panics if it encounters any error apart from a
// file not found.
func DiffDirectories(actual, expected string) (actualonly []string, expectedonly []string, different []Diff) {
seen := make(map[string]struct{})
filepath.Walk(expected, func(expectedPath string, expectedInfo os.FileInfo, err error) error {
if err != nil {
panic(err)
}
relPath := expectedPath[len(expected):]
seen[relPath] = struct{}{}
// ignore emacs backups
if strings.HasSuffix(expectedPath, "~") {
return nil
}
// ignore dotfiles
if strings.HasPrefix(filepath.Base(expectedPath), ".") {
if expectedInfo.IsDir() {
return filepath.SkipDir
}
return nil
}
actualPath := filepath.Join(actual, relPath)
actualInfo, err := os.Stat(actualPath)
switch {
case err == nil:
break
case os.IsNotExist(err):
expectedonly = append(expectedonly, relPath)
if expectedInfo.IsDir() {
return filepath.SkipDir
}
return nil
default:
panic(err)
}
// file exists in both places
switch {
case actualInfo.IsDir() && expectedInfo.IsDir():
return nil // i.e., keep recursing
case actualInfo.IsDir() || expectedInfo.IsDir():
different = append(different, dirFile{path: relPath, abspath: actualPath, expectedRegularFile: actualInfo.IsDir()})
if expectedInfo.IsDir() {
return filepath.SkipDir
}
return nil
}
// both regular files
actualBytes, err := os.ReadFile(actualPath)
if err != nil {
panic(err)
}
expectedBytes, err := os.ReadFile(expectedPath)
if err != nil {
panic(err)
}
if string(actualBytes) != string(expectedBytes) {
different = append(different, contentDiff{path: relPath, actual: string(actualBytes), expected: string(expectedBytes)})
}
return nil
})
// every file and directory in the actual result should be expected
filepath.Walk(actual, func(actualPath string, actualInfo os.FileInfo, err error) error {
if err != nil {
panic(err)
}
relPath := actualPath[len(actual):]
// ignore emacs backups
if strings.HasSuffix(actualPath, "~") {
return nil
}
// skip dotdirs
if actualInfo.IsDir() && strings.HasPrefix(filepath.Base(actualPath), ".") {
return filepath.SkipDir
}
if _, ok := seen[relPath]; !ok {
actualonly = append(actualonly, relPath)
if actualInfo.IsDir() {
return filepath.SkipDir
}
}
return nil
})
return
}