git-lfs/tools/filetools_test.go
Taylor Blau f7b1a5c8f8 tools/filetools.go: teach ExpandPath() to do path expansion
In order to expand strings of the form "~/path/to/hooks" into something
like "/home/ttaylorr/path/to/hooks", let's introduce the
tools.ExpandPath() function to do just that.

tools.ExpandPath() inspects the first character of the given string to
see if it begins with a "~" (indicating the presence of a home
directory.

If there is, see also if it refers to a specific "~user", and expand
appropriately.

To ensure that we can run the tests in tools/filetool_test.go in a
platform-independent manner, let's ensure that the results are
_semantically_ equal by first passing them through filepath.ToSlash().

We'll use this in the following commit in order to perform path
expansion on the value of core.hooksPath.
2018-08-30 16:59:33 -04:00

420 lines
11 KiB
Go

package tools
import (
"fmt"
"io/ioutil"
"os"
"os/user"
"path/filepath"
"runtime"
"sort"
"testing"
"github.com/git-lfs/git-lfs/subprocess"
"github.com/stretchr/testify/assert"
)
func TestCleanPathsCleansPaths(t *testing.T) {
cleaned := CleanPaths("/foo/bar/,/foo/bar/baz", ",")
assert.Equal(t, []string{"/foo/bar", "/foo/bar/baz"}, cleaned)
}
func TestCleanPathsReturnsNoResultsWhenGivenNoPaths(t *testing.T) {
cleaned := CleanPaths("", ",")
assert.Empty(t, cleaned)
}
type ExpandPathTestCase struct {
Path string
Expand bool
Want string
WantErr string
currentUser func() (*user.User, error)
lookupUser func(who string) (*user.User, error)
}
func (c *ExpandPathTestCase) Assert(t *testing.T) {
if c.currentUser != nil {
oldCurrentUser := currentUser
currentUser = c.currentUser
defer func() { currentUser = oldCurrentUser }()
}
if c.lookupUser != nil {
oldLookupUser := lookupUser
lookupUser = c.lookupUser
defer func() { lookupUser = oldLookupUser }()
}
got, err := ExpandPath(c.Path, c.Expand)
if err != nil || len(c.WantErr) > 0 {
assert.EqualError(t, err, c.WantErr)
}
assert.Equal(t, filepath.ToSlash(c.Want), filepath.ToSlash(got))
}
func TestExpandPath(t *testing.T) {
for desc, c := range map[string]*ExpandPathTestCase{
"no expand": {
Path: "/path/to/hooks",
Want: "/path/to/hooks",
},
"current": {
Path: "~/path/to/hooks",
Want: "/home/jane/path/to/hooks",
currentUser: func() (*user.User, error) {
return &user.User{
HomeDir: "/home/jane",
}, nil
},
},
"current, slash": {
Path: "~/",
Want: "/home/jane",
currentUser: func() (*user.User, error) {
return &user.User{
HomeDir: "/home/jane",
}, nil
},
},
"current, no slash": {
Path: "~",
Want: "/home/jane",
currentUser: func() (*user.User, error) {
return &user.User{
HomeDir: "/home/jane",
}, nil
},
},
"non-current": {
Path: "~other/path/to/hooks",
Want: "/home/special/path/to/hooks",
lookupUser: func(who string) (*user.User, error) {
assert.Equal(t, "other", who)
return &user.User{
HomeDir: "/home/special",
}, nil
},
},
"non-current, no slash": {
Path: "~other",
Want: "/home/special",
lookupUser: func(who string) (*user.User, error) {
assert.Equal(t, "other", who)
return &user.User{
HomeDir: "/home/special",
}, nil
},
},
"non-current (missing)": {
Path: "~other/path/to/hooks",
WantErr: "could not find user other: missing",
lookupUser: func(who string) (*user.User, error) {
assert.Equal(t, "other", who)
return nil, fmt.Errorf("missing")
},
},
} {
t.Run(desc, c.Assert)
}
}
func TestFastWalkBasic(t *testing.T) {
rootDir, err := ioutil.TempDir(os.TempDir(), "GitLfsTestFastWalkBasic")
if err != nil {
assert.FailNow(t, "Unable to get temp dir: %v", err)
}
defer os.RemoveAll(rootDir)
os.Chdir(rootDir)
expectedEntries := createFastWalkInputData(10, 160)
walker := fastWalkWithExcludeFiles(expectedEntries[0], "")
gotEntries, gotErrors := collectFastWalkResults(walker.ch)
assert.Empty(t, gotErrors)
sort.Strings(expectedEntries)
sort.Strings(gotEntries)
assert.Equal(t, expectedEntries, gotEntries)
}
func BenchmarkFastWalkGitRepoChannels(b *testing.B) {
rootDir, err := ioutil.TempDir(os.TempDir(), "GitLfsBenchFastWalkGitRepoChannels")
if err != nil {
assert.FailNow(b, "Unable to get temp dir: %v", err)
}
defer os.RemoveAll(rootDir)
os.Chdir(rootDir)
entries := createFastWalkInputData(1000, 5000)
for i := 0; i < b.N; i++ {
var files, errors int
FastWalkGitRepo(entries[0], func(parent string, info os.FileInfo, err error) {
if err != nil {
errors++
} else {
files++
}
})
b.Logf("files: %d, errors: %d", files, errors)
}
}
func BenchmarkFastWalkGitRepoCallback(b *testing.B) {
rootDir, err := ioutil.TempDir(os.TempDir(), "GitLfsBenchFastWalkGitRepoCallback")
if err != nil {
assert.FailNow(b, "Unable to get temp dir: %v", err)
}
defer os.RemoveAll(rootDir)
os.Chdir(rootDir)
entries := createFastWalkInputData(1000, 5000)
for i := 0; i < b.N; i++ {
var files, errors int
FastWalkGitRepo(entries[0], func(parentDir string, info os.FileInfo, err error) {
if err != nil {
errors++
} else {
files++
}
})
b.Logf("files: %d, errors: %d", files, errors)
}
}
func TestFastWalkGitRepo(t *testing.T) {
rootDir, err := ioutil.TempDir(os.TempDir(), "GitLfsTestFastWalkGitRepo")
if err != nil {
assert.FailNow(t, "Unable to get temp dir: %v", err)
}
defer os.RemoveAll(rootDir)
os.Chdir(rootDir)
expectedEntries := createFastWalkInputData(3, 3)
mainDir := expectedEntries[0]
// Set up a git repo and add some ignored files / dirs
subprocess.SimpleExec("git", "init", mainDir)
ignored := []string{
"filethatweignore.ign",
"foldercontainingignored",
"foldercontainingignored/notthisone.ign",
"foldercontainingignored/ignoredfolder",
"foldercontainingignored/ignoredfolder/file1.txt",
"foldercontainingignored/ignoredfolder/file2.txt",
"ignoredfolder",
"ignoredfolder/file1.txt",
"ignoredfolder/file2.txt",
"ignoredfrominside",
"ignoredfrominside/thisisok.txt",
"ignoredfrominside/thisisnot.txt",
"ignoredfrominside/thisone",
"ignoredfrominside/thisone/file1.txt",
}
for _, f := range ignored {
fullPath := join(mainDir, f)
if len(filepath.Ext(f)) > 0 {
ioutil.WriteFile(fullPath, []byte("TEST"), 0644)
} else {
os.MkdirAll(fullPath, 0755)
}
}
// write root .gitignore
rootGitIgnore := `
# ignore *.ign everywhere
*.ign
# ignore folder
ignoredfolder
`
ioutil.WriteFile(join(mainDir, ".gitignore"), []byte(rootGitIgnore), 0644)
// Subfolder ignore; folder will show up but but subfolder 'thisone' won't
subFolderIgnore := `
thisone
thisisnot.txt
`
ioutil.WriteFile(join(mainDir, "ignoredfrominside", ".gitignore"), []byte(subFolderIgnore), 0644)
// This dir will be walked but content won't be
expectedEntries = append(expectedEntries, join(mainDir, "foldercontainingignored"))
// This dir will be walked and some of its content but has its own gitignore
expectedEntries = append(expectedEntries, join(mainDir, "ignoredfrominside"))
expectedEntries = append(expectedEntries, join(mainDir, "ignoredfrominside", "thisisok.txt"))
// Also gitignores
expectedEntries = append(expectedEntries, join(mainDir, ".gitignore"))
expectedEntries = append(expectedEntries, join(mainDir, "ignoredfrominside", ".gitignore"))
// nothing else should be there
gotEntries := make([]string, 0, 1000)
gotErrors := make([]error, 0, 5)
FastWalkGitRepo(mainDir, func(parent string, info os.FileInfo, err error) {
if err != nil {
gotErrors = append(gotErrors, err)
} else {
if len(parent) == 0 {
gotEntries = append(gotEntries, info.Name())
} else {
gotEntries = append(gotEntries, join(parent, info.Name()))
}
}
})
assert.Empty(t, gotErrors)
sort.Strings(expectedEntries)
sort.Strings(gotEntries)
assert.Equal(t, expectedEntries, gotEntries)
// Go again using FastWalkGitRepoAll instead of FastWalkGitRepo to
// ensure that .gitingore'd files and directories are seen and
// traversed, respectively.
for _, ignore := range ignored {
expectedEntries = append(expectedEntries, join(mainDir, ignore))
}
gotEntries = make([]string, 0, 1000)
FastWalkGitRepoAll(mainDir, func(parent string, info os.FileInfo, err error) {
if err != nil {
gotErrors = append(gotErrors, err)
} else {
if len(parent) == 0 {
gotEntries = append(gotEntries, info.Name())
} else {
gotEntries = append(gotEntries, join(parent, info.Name()))
}
}
})
expectedEntries = uniq(expectedEntries)
gotEntries = uniq(gotEntries)
sort.Strings(expectedEntries)
sort.Strings(gotEntries)
assert.Equal(t, expectedEntries, gotEntries)
}
// Make test data - ensure you've Chdir'ed into a temp dir first
// Returns list of files/dirs that are created
// First entry is the parent dir of all others
func createFastWalkInputData(smallFolder, largeFolder int) []string {
dirs := []string{
"testroot",
"testroot/folder1",
"testroot/folder2",
"testroot/folder2/subfolder1",
"testroot/folder2/subfolder2",
"testroot/folder2/subfolder3",
"testroot/folder2/subfolder4",
"testroot/folder2/subfolder4/subsub",
}
expectedEntries := make([]string, 0, 250)
for i, dir := range dirs {
os.MkdirAll(dir, 0755)
numFiles := smallFolder
expectedEntries = append(expectedEntries, dir)
if i >= 3 && i <= 5 {
// Bulk test to ensure works with > 1 batch
numFiles = largeFolder
}
for f := 0; f < numFiles; f++ {
filename := join(dir, fmt.Sprintf("file%d.txt", f))
ioutil.WriteFile(filename, []byte("TEST"), 0644)
expectedEntries = append(expectedEntries, filename)
}
}
return expectedEntries
}
func collectFastWalkResults(fchan <-chan fastWalkInfo) ([]string, []error) {
gotEntries := make([]string, 0, 1000)
gotErrors := make([]error, 0, 5)
for o := range fchan {
if o.Err != nil {
gotErrors = append(gotErrors, o.Err)
} else {
if len(o.ParentDir) == 0 {
gotEntries = append(gotEntries, o.Info.Name())
} else {
gotEntries = append(gotEntries, join(o.ParentDir, o.Info.Name()))
}
}
}
return gotEntries, gotErrors
}
func getFileMode(filename string) os.FileMode {
s, err := os.Stat(filename)
if err != nil {
return 0000
}
return s.Mode()
}
// uniq creates an element-wise copy of "xs" containing only unique elements in
// the same order.
func uniq(xs []string) []string {
seen := make(map[string]struct{})
uniq := make([]string, 0, len(xs))
for _, x := range xs {
if _, ok := seen[x]; !ok {
seen[x] = struct{}{}
uniq = append(uniq, x)
}
}
return uniq
}
func TestSetWriteFlag(t *testing.T) {
f, err := ioutil.TempFile("", "lfstestwriteflag")
assert.Nil(t, err)
filename := f.Name()
defer os.Remove(filename)
f.Close()
// Set up with read/write bit for all but no execute
assert.Nil(t, os.Chmod(filename, 0666))
assert.Nil(t, SetFileWriteFlag(filename, false))
// should turn off all write
assert.EqualValues(t, 0444, getFileMode(filename))
assert.Nil(t, SetFileWriteFlag(filename, true))
// should only add back user write (on Mac/Linux)
if runtime.GOOS == "windows" {
assert.EqualValues(t, 0666, getFileMode(filename))
} else {
assert.EqualValues(t, 0644, getFileMode(filename))
}
// Can't run selective UGO tests on Windows as doesn't support separate roles
// Also Golang only supports read/write but not execute on Windows
if runtime.GOOS != "windows" {
// Set up with read/write/execute bit for all but no execute
assert.Nil(t, os.Chmod(filename, 0777))
assert.Nil(t, SetFileWriteFlag(filename, false))
// should turn off all write but not execute
assert.EqualValues(t, 0555, getFileMode(filename))
assert.Nil(t, SetFileWriteFlag(filename, true))
// should only add back user write (on Mac/Linux)
if runtime.GOOS == "windows" {
assert.EqualValues(t, 0777, getFileMode(filename))
} else {
assert.EqualValues(t, 0755, getFileMode(filename))
}
assert.Nil(t, os.Chmod(filename, 0440))
assert.Nil(t, SetFileWriteFlag(filename, false))
assert.EqualValues(t, 0440, getFileMode(filename))
assert.Nil(t, SetFileWriteFlag(filename, true))
// should only add back user write
assert.EqualValues(t, 0640, getFileMode(filename))
}
}