blob: 7753d96ca752cce680ff802d6606bfba90741927 [file] [log] [blame]
// Copyright 2017 The LUCI 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 filesystem
import (
"os"
"path/filepath"
"runtime"
"testing"
"time"
"go.chromium.org/luci/common/errors"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
func TestIsNotExist(t *testing.T) {
t.Parallel()
tdir := t.TempDir()
dnePath := filepath.Join(tdir, "anything")
_, err := os.Open(dnePath)
if !os.IsNotExist(err) {
t.Fatalf("failed to get IsNotExist error: %s", err)
}
Convey(`IsNotExist works`, t, func() {
So(IsNotExist(nil), ShouldBeFalse)
So(IsNotExist(errors.New("something")), ShouldBeFalse)
So(IsNotExist(err), ShouldBeTrue)
So(IsNotExist(errors.Annotate(err, "annotated").Err()), ShouldBeTrue)
})
}
func TestAbsPath(t *testing.T) {
t.Parallel()
tdir := t.TempDir()
tdirName := filepath.Base(tdir)
Convey(`AbsPath works`, t, func() {
base := filepath.Join(tdir, "..", tdirName, "file.txt")
So(AbsPath(&base), ShouldBeNil)
So(base, ShouldEqual, filepath.Join(tdir, "file.txt"))
})
}
func TestTouch(t *testing.T) {
t.Parallel()
Convey(`Testing Touch`, t, func() {
tdir := t.TempDir()
thePast := time.Now().Add(-10 * time.Second)
Convey(`Can update a directory timestamp`, func() {
path := filepath.Join(tdir, "subdir")
So(os.Mkdir(path, 0755), ShouldBeNil)
st, err := os.Lstat(path)
So(err, ShouldBeNil)
initialModTime := st.ModTime()
So(Touch(path, thePast, 0), ShouldBeNil)
st, err = os.Lstat(path)
So(err, ShouldBeNil)
So(st.ModTime(), ShouldHappenBefore, initialModTime)
})
Convey(`Can update an empty file timestamp`, func() {
path := filepath.Join(tdir, "touch")
So(Touch(path, time.Time{}, 0644), ShouldBeNil)
st, err := os.Lstat(path)
So(err, ShouldBeNil)
initialModTime := st.ModTime()
So(Touch(path, thePast, 0), ShouldBeNil)
st, err = os.Lstat(path)
So(err, ShouldBeNil)
pastModTime := st.ModTime()
So(pastModTime, ShouldHappenBefore, initialModTime)
// Touch back to "now".
So(Touch(path, time.Time{}, 0644), ShouldBeNil)
st, err = os.Lstat(path)
So(err, ShouldBeNil)
So(st.ModTime(), ShouldHappenOnOrAfter, initialModTime)
})
Convey(`Can update a populated file timestamp`, func() {
path := filepath.Join(tdir, "touch")
So(os.WriteFile(path, []byte("sup"), 0644), ShouldBeNil)
st, err := os.Lstat(path)
So(err, ShouldBeNil)
initialModTime := st.ModTime()
So(Touch(path, thePast, 0), ShouldBeNil)
st, err = os.Lstat(path)
So(err, ShouldBeNil)
So(st.ModTime(), ShouldHappenBefore, initialModTime)
content, err := os.ReadFile(path)
So(err, ShouldBeNil)
So(content, ShouldResemble, []byte("sup"))
})
})
}
func TestMakeReadOnly(t *testing.T) {
t.Parallel()
Convey(`Can RemoveAll`, t, func() {
tdir := t.TempDir()
defer func() {
So(recursiveChmod(tdir, nil, func(mode os.FileMode) os.FileMode { return mode | 0222 }), ShouldBeNil)
}()
for _, path := range []string{
filepath.Join(tdir, "foo", "bar"),
filepath.Join(tdir, "foo", "baz"),
filepath.Join(tdir, "qux"),
} {
base := filepath.Dir(path)
if err := os.MkdirAll(base, 0755); err != nil {
t.Fatalf("failed to populate directory [%s]: %s", base, err)
}
if err := os.WriteFile(path, []byte("junk"), 0644); err != nil {
t.Fatalf("failed to create file [%s]: %s", path, err)
}
}
Convey(`Can mark the entire subdirectory read-only`, func() {
So(MakeReadOnly(tdir, nil), ShouldBeNil)
filepath.Walk(tdir, func(path string, info os.FileInfo, err error) error {
So(err, ShouldBeNil)
So(info.Mode()&0222, ShouldEqual, 0)
return nil
})
})
Convey(`Can selectively mark files read-only`, func() {
// Don't mark <TMP>/foo read-only.
fooPath := filepath.Join(tdir, "foo")
So(MakeReadOnly(tdir, func(path string) bool {
return path != fooPath
}), ShouldBeNil)
filepath.Walk(tdir, func(path string, info os.FileInfo, err error) error {
So(err, ShouldBeNil)
if path == fooPath {
So(info.Mode()&0222, ShouldNotEqual, 0)
} else {
So(info.Mode()&0222, ShouldEqual, 0)
}
return nil
})
})
})
}
func TestRemoveAll(t *testing.T) {
t.Parallel()
Convey(`Can RemoveAll`, t, func() {
tdir := t.TempDir()
defer func() {
So(recursiveChmod(tdir, nil, func(mode os.FileMode) os.FileMode { return mode | 0222 }), ShouldBeNil)
}()
subDir := filepath.Join(tdir, "sub")
Convey(`With directory contents...`, func() {
for _, path := range []string{
filepath.Join(subDir, "foo", "bar"),
filepath.Join(subDir, "foo", "baz"),
filepath.Join(subDir, "qux"),
filepath.Join(subDir, "dummy"),
} {
base := filepath.Dir(path)
if err := os.MkdirAll(base, 0755); err != nil {
t.Fatalf("failed to populate directory [%s]: %s", base, err)
}
if err := os.WriteFile(path, []byte("junk"), 0644); err != nil {
t.Fatalf("failed to create file [%s]: %s", path, err)
}
}
// Make invalid symlink here.
So(os.Symlink("dummy", filepath.Join(subDir, "invalid_symlink")), ShouldBeNil)
So(os.Remove(filepath.Join(subDir, "dummy")), ShouldBeNil)
Convey(`Can remove the directory`, func() {
So(RemoveAll(subDir), ShouldBeNil)
So(isGone(subDir), ShouldBeTrue)
})
Convey(`Removing inside a read-only dir...`, func() {
So(MakeReadOnly(subDir, nil), ShouldBeNil)
qux := filepath.Join(subDir, "qux")
if runtime.GOOS == "windows" {
Convey(`... is fine on Windows`, func() {
So(RemoveAll(qux), ShouldBeNil)
So(isGone(qux), ShouldBeTrue)
})
} else {
Convey(`... fails on POSIX`, func() {
So(RemoveAll(qux), ShouldNotBeNil)
So(isGone(qux), ShouldBeFalse)
})
}
})
Convey(`Can remove the directory when it is read-only`, func() {
// Make the directory read-only, and assert that classic os.RemoveAll
// fails. Except on Windows it doesn't, see:
// https://github.com/golang/go/issues/26295.
So(MakeReadOnly(subDir, nil), ShouldBeNil)
if runtime.GOOS != "windows" {
So(os.RemoveAll(subDir), ShouldNotBeNil)
}
So(RemoveAll(subDir), ShouldBeNil)
So(isGone(subDir), ShouldBeTrue)
})
Convey(`Can remove the directory when it has read-only files`, func() {
readOnly := filepath.Join(subDir, "foo", "bar")
So(MakeReadOnly(readOnly, nil), ShouldBeNil)
So(RemoveAll(subDir), ShouldBeNil)
So(isGone(subDir), ShouldBeTrue)
})
})
Convey(`Will return nil if the target does not exist`, func() {
So(RemoveAll(filepath.Join(tdir, "dne")), ShouldBeNil)
})
})
}
func TestRenamingRemoveAll(t *testing.T) {
t.Parallel()
Convey(`Can RenamingRemoveAll`, t, func() {
tdir := t.TempDir()
subDir := filepath.Join(tdir, "sub")
fooDir := filepath.Join(subDir, "foo")
fooBarFile := filepath.Join(fooDir, "bar")
if err := os.MkdirAll(fooDir, 0755); err != nil {
t.Fatalf("failed to populate directory [%s]: %s", fooDir, err)
}
if err := os.WriteFile(fooBarFile, []byte("junk"), 0644); err != nil {
t.Fatalf("failed to create file [%s]: %s", fooBarFile, err)
}
Convey(`No dir to rename into specified`, func() {
renamedTo, err := RenamingRemoveAll(fooDir, "")
So(err, ShouldBeNil)
So(isGone(fooDir), ShouldBeTrue)
So(renamedTo, ShouldStartWith, filepath.Join(subDir, ".trash-"))
})
Convey(`Renaming to specified dir works`, func() {
renamedTo, err := RenamingRemoveAll(fooDir, tdir)
So(err, ShouldBeNil)
So(isGone(fooDir), ShouldBeTrue)
So(renamedTo, ShouldStartWith, filepath.Join(tdir, ".trash-"))
})
Convey(`Renaming to specified dir fails due to not exist error`, func() {
renamedTo, err := RenamingRemoveAll(fooDir, filepath.Join(tdir, "not-exists"))
So(err, ShouldBeNil)
So(isGone(fooDir), ShouldBeTrue)
So(renamedTo, ShouldEqual, "")
})
Convey(`Will return nil if the target does not exist`, func() {
_, err := RenamingRemoveAll(filepath.Join(tdir, "not-exists"), "")
So(err, ShouldBeNil)
})
})
}
func TestReadableCopy(t *testing.T) {
Convey("ReadableCopy", t, func() {
dir := t.TempDir()
out := filepath.Join(dir, "out")
in := filepath.Join(dir, "in")
content := []byte("test")
So(os.WriteFile(in, content, 0644), ShouldBeNil)
// Change umask on unix so that test is not affected by default umask.
old := umask(022)
So(ReadableCopy(out, in), ShouldBeNil)
umask(old)
buf, err := os.ReadFile(out)
So(err, ShouldBeNil)
So(buf, ShouldResemble, content)
ostat, err := os.Stat(out)
So(err, ShouldBeNil)
istat, err := os.Stat(in)
So(err, ShouldBeNil)
So(ostat.Mode(), ShouldEqual, addReadMode(istat.Mode()))
})
}
func TestCopy(t *testing.T) {
Convey("Copy", t, func() {
dir := t.TempDir()
out := filepath.Join(dir, "out")
in := filepath.Join(dir, "in")
content := []byte("test")
So(os.WriteFile(in, content, 0o644), ShouldBeNil)
// Change umask on unix so that test is not affected by default umask.
old := umask(0o22)
So(Copy(out, in, 0o644), ShouldBeNil)
umask(old)
buf, err := os.ReadFile(out)
So(err, ShouldBeNil)
So(buf, ShouldResemble, content)
ostat, err := os.Stat(out)
So(err, ShouldBeNil)
_, err = os.Stat(in)
So(err, ShouldBeNil)
if runtime.GOOS != "windows" {
So(ostat.Mode(), ShouldEqual, 0o644)
}
})
}
func TestHardlinkRecursively(t *testing.T) {
t.Parallel()
checkFile := func(path, content string) {
buf, err := os.ReadFile(path)
So(err, ShouldBeNil)
So(string(buf), ShouldResemble, content)
}
Convey("HardlinkRecursively", t, func() {
dir := t.TempDir()
src1 := filepath.Join(dir, "src1")
src2 := filepath.Join(src1, "src2")
So(os.MkdirAll(src2, 0755), ShouldBeNil)
So(os.WriteFile(filepath.Join(src1, "file1"), []byte("test1"), 0644), ShouldBeNil)
So(os.WriteFile(filepath.Join(src2, "file2"), []byte("test2"), 0644), ShouldBeNil)
So(os.Symlink(filepath.Join("src2", "file2"), filepath.Join(src1, "link")), ShouldBeNil)
dst := filepath.Join(dir, "dst")
So(HardlinkRecursively(src1, dst), ShouldBeNil)
checkFile(filepath.Join(dst, "file1"), "test1")
checkFile(filepath.Join(dst, "src2", "file2"), "test2")
checkFile(filepath.Join(dst, "link"), "test2")
stat, err := os.Stat(filepath.Join(dst, "link"))
So(err, ShouldBeNil)
So(stat.Mode()&os.ModeSymlink, ShouldEqual, 0)
})
Convey("HardlinkRecursively nested", t, func() {
dir := t.TempDir()
top := filepath.Join(dir, "top")
nested := filepath.Join(top, "nested")
So(os.MkdirAll(nested, 0755), ShouldBeNil)
So(os.WriteFile(filepath.Join(nested, "file"), []byte("test"), 0644), ShouldBeNil)
dstTop := filepath.Join(dir, "dst")
So(HardlinkRecursively(top, dstTop), ShouldBeNil)
dstDested := filepath.Join(dstTop, "nested")
So(HardlinkRecursively(nested, dstDested), ShouldNotBeNil)
})
}
func TestCreateDirectories(t *testing.T) {
t.Parallel()
Convey("CreateDirectories", t, func() {
dir := t.TempDir()
So(CreateDirectories(dir, []string{}), ShouldBeNil)
var paths []string
correctFiles := func(path string, info os.FileInfo, err error) error {
paths = append(paths, path)
return nil
}
So(filepath.Walk(dir, correctFiles), ShouldBeNil)
So(paths, ShouldResemble, []string{dir})
So(CreateDirectories(dir, []string{
filepath.Join("a", "b"),
filepath.Join("c", "d", "e"),
filepath.Join("c", "f"),
filepath.Join("g"),
}), ShouldBeNil)
paths = nil
So(filepath.Walk(dir, correctFiles), ShouldBeNil)
So(paths, ShouldResemble, []string{
dir,
filepath.Join(dir, "a"),
filepath.Join(dir, "c"),
filepath.Join(dir, "c", "d"),
})
})
}
func TestIsEmptyDir(t *testing.T) {
Convey("IsEmptyDir", t, func() {
dir := t.TempDir()
emptyDir := filepath.Join(dir, "empty")
So(MakeDirs(emptyDir), ShouldBeNil)
isEmpty, err := IsEmptyDir(emptyDir)
So(err, ShouldBeNil)
So(isEmpty, ShouldBeTrue)
nonEmptyDir := filepath.Join(dir, "non-empty")
So(MakeDirs(nonEmptyDir), ShouldBeNil)
file1 := filepath.Join(nonEmptyDir, "file1")
So(os.WriteFile(file1, []byte("test1"), 0644), ShouldBeNil)
isEmpty, err = IsEmptyDir(nonEmptyDir)
So(err, ShouldBeNil)
So(isEmpty, ShouldBeFalse)
_, err = IsEmptyDir(file1)
So(err, ShouldNotBeNil)
_, err = IsEmptyDir(filepath.Join(dir, "not-existing"))
So(err, ShouldNotBeNil)
})
}
func TestIsDir(t *testing.T) {
Convey("IsDir", t, func() {
dir := t.TempDir()
b, err := IsDir(dir)
So(err, ShouldBeNil)
So(b, ShouldBeTrue)
b, err = IsDir(filepath.Join(dir, "not_exists"))
So(err, ShouldBeNil)
So(b, ShouldBeFalse)
file := filepath.Join(dir, "file")
So(os.WriteFile(file, []byte(""), 0644), ShouldBeNil)
b, err = IsDir(file)
So(err, ShouldBeNil)
So(b, ShouldBeFalse)
})
}
func TestGetFreeSpace(t *testing.T) {
t.Parallel()
Convey("GetFreeSpace", t, func() {
size, err := GetFreeSpace(".")
So(err, ShouldBeNil)
So(size, ShouldBeGreaterThan, 4096)
})
}
func isGone(path string) bool {
if _, err := os.Lstat(path); err != nil {
return os.IsNotExist(err)
}
return false
}
func TestGetCommonAncestor(t *testing.T) {
t.Parallel()
tdir := t.TempDir()
if err := Touch(filepath.Join(tdir, "A"), time.Now(), 0o666); err != nil {
t.Error(err)
}
if err := Touch(filepath.Join(tdir, "a"), time.Now(), 0o666); err != nil {
t.Error(err)
}
big, err := os.Stat(filepath.Join(tdir, "A"))
if err != nil {
t.Error(err)
}
small, err := os.Stat(filepath.Join(tdir, "a"))
if err != nil {
t.Error(err)
}
fsCaseSensitive := !os.SameFile(big, small)
const sep = string(filepath.Separator)
Convey(`GetCommonAncestor`, t, func() {
Convey(`common ancestor logic`, func() {
Convey(`common`, func() {
tdir := t.TempDir()
So(os.MkdirAll(filepath.Join(tdir, "a", "b", "c", "d"), 0o777), ShouldBeNil)
if fsCaseSensitive {
So(os.MkdirAll(filepath.Join(tdir, "a", "B", "c"), 0o777), ShouldBeNil)
So(os.MkdirAll(filepath.Join(tdir, "A", "b", "c", "d"), 0o777), ShouldBeNil)
}
So(os.MkdirAll(filepath.Join(tdir, "a", "1", "2", "3"), 0o777), ShouldBeNil)
So(os.MkdirAll(filepath.Join(tdir, "r", "o", "o", "t"), 0o777), ShouldBeNil)
So(Touch(filepath.Join(tdir, "a", "B", "c", "something"), time.Now(), 0o666), ShouldBeNil)
So(Touch(filepath.Join(tdir, "a", "b", "else"), time.Now(), 0o666), ShouldBeNil)
Convey(`stops at 'local' common ancestor`, func() {
common, err := GetCommonAncestor([]string{
filepath.Join(tdir, "a", "b", "c", "d"),
filepath.Join(tdir, "a", "B", "c", "something"),
filepath.Join(tdir, "A", "b", "c", "d"),
filepath.Join(tdir, "a", "b", "else"),
}, []string{".git"})
So(err, ShouldBeNil)
if fsCaseSensitive {
So(common, ShouldResemble, tdir+sep)
} else {
So(common, ShouldResemble, filepath.Join(tdir, "a", "b")+sep)
}
})
Convey(`walks up to fs root`, func() {
common, err := GetCommonAncestor([]string{
filepath.VolumeName(tdir) + sep,
filepath.Join(tdir, "a", "B", "c", "something"),
filepath.Join(tdir, "A", "b", "c", "d"),
filepath.Join(tdir, "a", "b", "else"),
}, []string{".git"})
So(err, ShouldBeNil)
So(common, ShouldResemble, filepath.VolumeName(tdir)+sep)
})
})
Convey(`slash comparison`, func() {
tdir := t.TempDir()
So(os.MkdirAll(filepath.Join(tdir, "longpath", "a", "c", "d"), 0o777), ShouldBeNil)
So(os.MkdirAll(filepath.Join(tdir, "a", "longpath", "c", "d"), 0o777), ShouldBeNil)
common, err := GetCommonAncestor([]string{
filepath.Join(tdir, "longpath", "a", "c", "d"),
filepath.Join(tdir, "a", "longpath", "c", "d"),
}, []string{".git"})
So(err, ShouldBeNil)
So(common, ShouldResemble, tdir+sep)
})
Convey(`non exist`, func() {
_, err := GetCommonAncestor([]string{
filepath.Join("i", "dont", "exist"),
}, []string{".git"})
So(err, ShouldErrLike, "reading path[0]")
})
Convey(`sentinel`, func() {
tdir := t.TempDir()
So(os.MkdirAll(filepath.Join(tdir, "repoA", ".git"), 0o777), ShouldBeNil)
So(os.MkdirAll(filepath.Join(tdir, "repoA", "something"), 0o777), ShouldBeNil)
So(os.MkdirAll(filepath.Join(tdir, "repoB", ".git"), 0o777), ShouldBeNil)
So(os.MkdirAll(filepath.Join(tdir, "repoB", "else"), 0o777), ShouldBeNil)
_, err := GetCommonAncestor([]string{
filepath.Join(tdir, "repoA", "something"),
filepath.Join(tdir, "repoB", "else"),
}, []string{".git"})
So(err, ShouldErrLike, "hit root sentinel")
So(errors.Is(err, ErrRootSentinel), ShouldBeTrue)
})
if runtime.GOOS == "windows" {
Convey(`different volumes`, func() {
tdir := t.TempDir()
_, err := GetCommonAncestor([]string{
tdir,
`\\fake\network\share`,
}, nil)
So(err, ShouldErrLike, "originate on different volumes")
})
}
})
Convey(`helpers`, func() {
if runtime.GOOS == "windows" {
Convey(`windows paths`, func() {
So(findPathSeparators(`D:\something\`), ShouldResemble, []int{2, 12})
So(findPathSeparators(`D:\`), ShouldResemble, []int{2})
So(findPathSeparators(`\\some\host\something\`),
ShouldResemble, []int{11, 21})
So(findPathSeparators(`\\some\host\`),
ShouldResemble, []int{11})
So(findPathSeparators(`\\?\C:\Test\`),
ShouldResemble, []int{6, 11})
})
} else {
Convey(`*nix paths`, func() {
So(findPathSeparators(`/something/`),
ShouldResemble, []int{0, 10})
So(findPathSeparators(`/`),
ShouldResemble, []int{0})
})
}
})
})
}