blob: 9a11bb27d4b50897dc6989b99b9072239be3ac20 [file] [log] [blame]
// Copyright 2014 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 fs
import (
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestCaseSensitive(t *testing.T) {
t.Parallel()
Convey("CaseSensitive doesn't crash", t, func(c C) {
fs := tempFileSystem(c)
_, err := fs.CaseSensitive()
So(err, ShouldBeNil)
// No garbage left around.
fis, err := ioutil.ReadDir(fs.join(""))
So(err, ShouldBeNil)
So(fis, ShouldHaveLength, 0)
})
}
func TestCwdRelToAbs(t *testing.T) {
t.Parallel()
Convey("CwdRelToAbs works", t, func(c C) {
fs := tempFileSystem(c)
p, err := fs.CwdRelToAbs(fs.Root())
So(err, ShouldBeNil)
So(p, ShouldEqual, fs.Root())
p, err = fs.CwdRelToAbs(fs.join("already_abs/def"))
So(err, ShouldBeNil)
So(p, ShouldEqual, fs.join("already_abs/def"))
_, err = fs.CwdRelToAbs(fs.join(".."))
So(err, ShouldNotBeNil)
_, err = fs.CwdRelToAbs(fs.join("../.."))
So(err, ShouldNotBeNil)
_, err = fs.CwdRelToAbs(fs.join("../abc"))
So(err, ShouldNotBeNil)
})
}
func TestRootRelToAbs(t *testing.T) {
t.Parallel()
Convey("RootRelToAbs works", t, func(c C) {
fs := tempFileSystem(c)
p, err := fs.RootRelToAbs(".")
So(err, ShouldBeNil)
So(p, ShouldEqual, fs.Root())
p, err = fs.RootRelToAbs(fs.join("already_abs/def"))
So(err, ShouldBeNil)
So(p, ShouldEqual, fs.join("already_abs/def"))
_, err = fs.RootRelToAbs(filepath.FromSlash("abc/../../def"))
So(err, ShouldNotBeNil)
})
}
func TestEnsureDirectory(t *testing.T) {
t.Parallel()
ctx := context.Background()
Convey("EnsureDirectory checks root", t, func(c C) {
fs := tempFileSystem(c)
_, err := fs.EnsureDirectory(ctx, fs.join(".."))
So(err, ShouldNotBeNil)
})
Convey("EnsureDirectory works", t, func(c C) {
fs := tempFileSystem(c)
p, err := fs.EnsureDirectory(ctx, fs.join("x/../y/z"))
So(err, ShouldBeNil)
So(p, ShouldEqual, fs.join("y/z"))
So(fs.isDir("y/z"), ShouldBeTrue)
// Same one.
_, err = fs.EnsureDirectory(ctx, fs.join("x/../y/z"))
So(err, ShouldBeNil)
})
Convey("EnsureDirectory replaces files with directories", t, func(c C) {
fs := tempFileSystem(c)
fs.write("a", "xxx")
_, err := fs.EnsureDirectory(ctx, fs.join("a/b/c"))
So(err, ShouldBeNil)
So(fs.isDir("a/b/c"), ShouldBeTrue)
})
if runtime.GOOS != "windows" {
Convey("EnsureDirectory replaces file symlinks with directories", t, func(c C) {
fs := tempFileSystem(c)
fs.write("target", "xxx")
fs.symlink("target", "a")
_, err := fs.EnsureDirectory(ctx, fs.join("a"))
So(err, ShouldBeNil)
So(fs.isDir("a"), ShouldBeTrue)
})
Convey("EnsureDirectory replaces broken symlinks with directories", t, func(c C) {
fs := tempFileSystem(c)
fs.symlink("broken", "a")
_, err := fs.EnsureDirectory(ctx, fs.join("a"))
So(err, ShouldBeNil)
So(fs.isDir("a"), ShouldBeTrue)
})
}
}
func TestEnsureSymlink(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
t.Skip("Skipping on Windows: no symlinks")
}
ctx := context.Background()
Convey("EnsureSymlink checks root", t, func(c C) {
fs := tempFileSystem(c)
So(fs.EnsureSymlink(ctx, fs.join(".."), fs.Root()), ShouldNotBeNil)
})
Convey("EnsureSymlink creates new symlink", t, func(c C) {
fs := tempFileSystem(c)
So(fs.EnsureSymlink(ctx, fs.join("symlink"), "target"), ShouldBeNil)
So(fs.readLink("symlink"), ShouldEqual, "target")
})
Convey("EnsureSymlink builds full path", t, func(c C) {
fs := tempFileSystem(c)
So(fs.EnsureSymlink(ctx, fs.join("a/b/c"), "target"), ShouldBeNil)
So(fs.readLink("a/b/c"), ShouldEqual, "target")
})
Convey("EnsureSymlink replaces existing symlink", t, func(c C) {
fs := tempFileSystem(c)
// Replace with same one, then with another one.
So(fs.EnsureSymlink(ctx, fs.join("symlink"), "target"), ShouldBeNil)
So(fs.EnsureSymlink(ctx, fs.join("symlink"), "target"), ShouldBeNil)
So(fs.EnsureSymlink(ctx, fs.join("symlink"), "another"), ShouldBeNil)
So(fs.readLink("symlink"), ShouldEqual, "another")
})
Convey("EnsureSymlink replaces existing file", t, func(c C) {
fs := tempFileSystem(c)
fs.write("path", "blah")
So(fs.EnsureSymlink(ctx, fs.join("path"), "target"), ShouldBeNil)
So(fs.readLink("path"), ShouldEqual, "target")
})
Convey("EnsureSymlink replaces existing directory", t, func(c C) {
fs := tempFileSystem(c)
fs.write("a/b/c", "something")
So(fs.EnsureSymlink(ctx, fs.join("a"), "target"), ShouldBeNil)
So(fs.readLink("a"), ShouldEqual, "target")
})
}
func TestEnsureFile(t *testing.T) {
t.Parallel()
ctx := context.Background()
Convey("EnsureFile checks root", t, func(c C) {
fs := tempFileSystem(c)
So(EnsureFile(ctx, fs, fs.join(".."), strings.NewReader("blah")), ShouldNotBeNil)
})
Convey("EnsureFile creates new file", t, func(c C) {
fs := tempFileSystem(c)
So(EnsureFile(ctx, fs, fs.join("name"), strings.NewReader("blah")), ShouldBeNil)
So(fs.read("name"), ShouldEqual, "blah")
})
Convey("EnsureFile builds full path", t, func(c C) {
fs := tempFileSystem(c)
So(EnsureFile(ctx, fs, fs.join("a/b/c"), strings.NewReader("blah")), ShouldBeNil)
So(fs.read("a/b/c"), ShouldEqual, "blah")
})
if runtime.GOOS != "windows" {
Convey("EnsureFile replaces existing symlink", t, func(c C) {
fs := tempFileSystem(c)
So(fs.EnsureSymlink(ctx, fs.join("path"), "target"), ShouldBeNil)
So(EnsureFile(ctx, fs, fs.join("path"), strings.NewReader("blah")), ShouldBeNil)
So(fs.read("path"), ShouldEqual, "blah")
})
}
Convey("EnsureFile replaces existing file", t, func(c C) {
fs := tempFileSystem(c)
So(EnsureFile(ctx, fs, fs.join("path"), strings.NewReader("huh")), ShouldBeNil)
So(EnsureFile(ctx, fs, fs.join("path"), strings.NewReader("blah")), ShouldBeNil)
So(fs.read("path"), ShouldEqual, "blah")
})
Convey("EnsureFile replaces existing file while it is being read", t, func(c C) {
fs := tempFileSystem(c)
// Create an initial file, then open it for reading.
So(EnsureFile(ctx, fs, fs.join("path"), strings.NewReader("huh")), ShouldBeNil)
fd, err := fs.OpenFile(fs.join("path"))
So(err, ShouldBeNil)
defer fd.Close()
// Ensure a second file on top of it.
So(EnsureFile(ctx, fs, fs.join("path"), strings.NewReader("blah")), ShouldBeNil)
// Read and verify the content of the first file.
content, err := ioutil.ReadAll(fd)
So(err, ShouldBeNil)
So(content, ShouldResemble, []byte("huh"))
// Verify the content of the second ensured file.
So(fs.read("path"), ShouldEqual, "blah")
// The first file should successfully close.
So(fd.Close(), ShouldBeNil)
})
Convey("EnsureFile replaces existing file while it is being written", t, func(c C) {
fs := tempFileSystem(c)
// Start one EnsureFile, pausing in the middle so another can start
// alongside it.
firstEnsureStartedC := make(chan struct{})
finishFirstWriteC := make(chan struct{})
errC := make(chan error)
go func() {
errC <- fs.EnsureFile(ctx, fs.join("path"), func(fd *os.File) error {
close(firstEnsureStartedC)
<-finishFirstWriteC
_, err := fd.Write([]byte("foo"))
return err
})
}()
// Wait for the first to signal that it's begin, then perform a second
// overlapping EnsureFile.
<-firstEnsureStartedC
So(EnsureFile(ctx, fs, fs.join("path"), strings.NewReader("overwrite me")), ShouldBeNil)
close(finishFirstWriteC)
So(<-errC, ShouldBeNil)
// The first EnsureFile content should be the one that is present.
So(fs.read("path"), ShouldEqual, "foo")
})
Convey("EnsureFile replaces existing directory", t, func(c C) {
fs := tempFileSystem(c)
fs.write("a/b/c", "something")
So(EnsureFile(ctx, fs, fs.join("a"), strings.NewReader("blah")), ShouldBeNil)
So(fs.read("a"), ShouldEqual, "blah")
})
}
func TestEnsureFileGone(t *testing.T) {
t.Parallel()
ctx := context.Background()
Convey("EnsureFileGone checks root", t, func(c C) {
fs := tempFileSystem(c)
So(fs.EnsureFileGone(ctx, fs.join("../abc")), ShouldNotBeNil)
})
Convey("EnsureFileGone works", t, func(c C) {
fs := tempFileSystem(c)
fs.write("abc", "")
So(fs.EnsureFileGone(ctx, fs.join("abc")), ShouldBeNil)
So(fs.isMissing("abc"), ShouldBeTrue)
})
Convey("EnsureFileGone works with missing file", t, func(c C) {
fs := tempFileSystem(c)
So(fs.EnsureFileGone(ctx, fs.join("abc")), ShouldBeNil)
})
Convey("EnsureFileGone works with empty directory", t, func(c C) {
fs := tempFileSystem(c)
fs.mkdir("abc")
So(fs.EnsureFileGone(ctx, fs.join("abc")), ShouldBeNil)
So(fs.isMissing("abc"), ShouldBeTrue)
})
Convey("EnsureFileGone fails with non-empty directory", t, func(c C) {
fs := tempFileSystem(c)
fs.write("abc/file", "zzz")
So(fs.EnsureFileGone(ctx, fs.join("abc")), ShouldNotBeNil)
})
Convey("EnsureFileGone skips paths that use files as if they are dirs", t, func(c C) {
fs := tempFileSystem(c)
fs.write("abc/file", "zzz")
So(fs.EnsureFileGone(ctx, fs.join("abc/file/huh")), ShouldBeNil)
})
if runtime.GOOS != "windows" {
Convey("EnsureFileGone works with symlink", t, func(c C) {
fs := tempFileSystem(c)
So(fs.EnsureSymlink(ctx, fs.join("abc"), "target"), ShouldBeNil)
So(fs.EnsureFileGone(ctx, fs.join("abc")), ShouldBeNil)
So(fs.isMissing("abc"), ShouldBeTrue)
})
}
Convey("crbug.com/936911: EnsureFileGone can delete files opened with OpenFile", t, func(c C) {
fs := tempFileSystem(c)
fs.write("abc/file", "zzz")
f, err := fs.OpenFile(fs.join("abc/file"))
So(err, ShouldBeNil)
So(f.Close(), ShouldBeNil)
So(fs.EnsureFileGone(ctx, f.Name()), ShouldBeNil)
})
}
func TestEnsureDirectoryGone(t *testing.T) {
t.Parallel()
ctx := context.Background()
Convey("EnsureDirectoryGone checks root", t, func(c C) {
fs := tempFileSystem(c)
So(fs.EnsureDirectoryGone(ctx, fs.join("../abc")), ShouldNotBeNil)
})
Convey("EnsureDirectoryGone works", t, func(c C) {
fs := tempFileSystem(c)
fs.write("dir/a/1", "")
fs.write("dir/a/2", "")
fs.write("dir/b/1", "")
So(fs.EnsureDirectoryGone(ctx, fs.join("dir")), ShouldBeNil)
So(fs.isMissing("dir"), ShouldBeTrue)
})
Convey("EnsureDirectoryGone works with missing dir", t, func(c C) {
fs := tempFileSystem(c)
So(fs.EnsureDirectoryGone(ctx, fs.join("missing")), ShouldBeNil)
})
}
func TestReplace(t *testing.T) {
t.Parallel()
ctx := context.Background()
Convey("Replace checks root", t, func(c C) {
fs := tempFileSystem(c)
So(fs.Replace(ctx, fs.join("../abc"), fs.join("def")), ShouldNotBeNil)
fs.write("def", "")
So(fs.Replace(ctx, fs.join("def"), fs.join("../abc")), ShouldNotBeNil)
})
Convey("Replace does nothing if oldpath == newpath", t, func(c C) {
fs := tempFileSystem(c)
So(fs.Replace(ctx, fs.join("abc"), fs.join("abc/d/..")), ShouldBeNil)
})
Convey("Replace recognizes missing oldpath", t, func(c C) {
fs := tempFileSystem(c)
So(fs.Replace(ctx, fs.join("missing"), fs.join("abc")), ShouldNotBeNil)
})
Convey("Replace creates file and dir if missing", t, func(c C) {
fs := tempFileSystem(c)
fs.write("a/123", "")
So(fs.Replace(ctx, fs.join("a/123"), fs.join("b/c/d")), ShouldBeNil)
So(fs.isMissing("a/123"), ShouldBeTrue)
So(fs.isFile("b/c/d/"), ShouldBeTrue)
})
Convey("Replace replaces regular file with a file", t, func(c C) {
fs := tempFileSystem(c)
fs.write("a/123", "xxx")
fs.write("b/c/d", "yyy")
So(fs.Replace(ctx, fs.join("a/123"), fs.join("b/c/d")), ShouldBeNil)
So(fs.isMissing("a/123"), ShouldBeTrue)
So(fs.read("b/c/d"), ShouldEqual, "xxx")
})
Convey("Replace replaces regular file with a dir", t, func(c C) {
fs := tempFileSystem(c)
fs.write("a/123/456", "xxx")
fs.write("b/c/d", "yyy")
So(fs.Replace(ctx, fs.join("a/123"), fs.join("b/c/d")), ShouldBeNil)
So(fs.isMissing("a/123"), ShouldBeTrue)
So(fs.read("b/c/d/456"), ShouldEqual, "xxx")
})
Convey("Replace replaces empty dir with a file", t, func(c C) {
fs := tempFileSystem(c)
fs.write("a/123", "xxx")
_, err := fs.EnsureDirectory(ctx, fs.join("b/c/d"))
So(err, ShouldBeNil)
So(fs.Replace(ctx, fs.join("a/123"), fs.join("b/c/d")), ShouldBeNil)
So(fs.isMissing("a/123"), ShouldBeTrue)
So(fs.read("b/c/d"), ShouldEqual, "xxx")
})
Convey("Replace replaces empty dir with a dir", t, func(c C) {
fs := tempFileSystem(c)
fs.write("a/123/456", "xxx")
_, err := fs.EnsureDirectory(ctx, fs.join("b/c/d"))
So(err, ShouldBeNil)
So(fs.Replace(ctx, fs.join("a/123"), fs.join("b/c/d")), ShouldBeNil)
So(fs.isMissing("a/123"), ShouldBeTrue)
So(fs.read("b/c/d/456"), ShouldEqual, "xxx")
})
Convey("Replace replaces dir with a file", t, func(c C) {
fs := tempFileSystem(c)
fs.write("a/123", "xxx")
fs.write("b/c/d/456", "yyy")
So(fs.Replace(ctx, fs.join("a/123"), fs.join("b/c/d")), ShouldBeNil)
So(fs.isMissing("a/123"), ShouldBeTrue)
So(fs.read("b/c/d"), ShouldEqual, "xxx")
})
Convey("Replace replaces dir with a dir", t, func(c C) {
fs := tempFileSystem(c)
fs.write("a/123/456", "xxx")
fs.write("b/c/d/456", "yyy")
So(fs.Replace(ctx, fs.join("a/123"), fs.join("b/c/d")), ShouldBeNil)
So(fs.isMissing("a/123"), ShouldBeTrue)
So(fs.read("b/c/d/456"), ShouldEqual, "xxx")
})
Convey("Replace replaces file in dir path with a directory", t, func(c C) {
fs := tempFileSystem(c)
fs.write("a/123", "xxx")
fs.write("f", "yyy")
So(fs.Replace(ctx, fs.join("f"), fs.join("a/123/456")), ShouldBeNil)
So(fs.read("a/123/456"), ShouldEqual, "yyy")
})
Convey("Replace works for very long paths", t, func(c C) {
fs := tempFileSystem(c)
// Windows runs into trouble with paths > 260 chars
longPath1 := strings.Repeat("abc/", 260/4) + "file"
longPath2 := strings.Repeat("xyz/", 260/4) + "file"
So(len(longPath1), ShouldBeGreaterThan, 260)
fs.write(longPath1, "hi")
fs.write(longPath2, "there")
So(fs.Replace(ctx, fs.join(longPath1), fs.join(longPath2)), ShouldBeNil)
So(fs.read(longPath2), ShouldEqual, "hi")
})
Convey("Concurrency test", t, func(c C) {
const threads = 50
fs := tempFileSystem(c)
wg := sync.WaitGroup{}
wg.Add(threads)
for th := 0; th < threads; th++ {
th := th
go func() {
defer wg.Done()
var oldpath string
switch th % 3 {
case 0: // replacing with a file
oldpath = fmt.Sprintf("file_%d", th)
fs.write(oldpath, "...")
case 1: // replacing with an empty directory
oldpath = fmt.Sprintf("empty_%d", th)
fs.mkdir(oldpath)
case 2: // replacing with a non-empty directory
oldpath = fmt.Sprintf("dir_%d", th)
fs.write(oldpath+"/file", "...")
}
if err := fs.Replace(ctx, fs.join(oldpath), fs.join("new/path")); err != nil {
panic(err)
}
}()
}
wg.Wait()
})
}
///////
// tempFileSystem returns FileSystem for tests built over a temp directory.
func tempFileSystem(c C) *tempFileSystemImpl {
tempDir, err := ioutil.TempDir("", "cipd_test")
So(err, ShouldBeNil)
Reset(func() { os.RemoveAll(tempDir) })
return &tempFileSystemImpl{NewFileSystem(tempDir, ""), c}
}
type tempFileSystemImpl struct {
FileSystem
c C
}
// join returns absolute path given a slash separated path relative to Root().
func (f *tempFileSystemImpl) join(path string) string {
return filepath.Join(f.Root(), filepath.FromSlash(path))
}
// write creates a file at a given slash separated path relative to Root().
func (f *tempFileSystemImpl) write(rel string, data string) {
abs := f.join(rel)
err := os.MkdirAll(filepath.Dir(abs), 0777)
f.c.So(err, ShouldBeNil)
file, err := os.Create(abs)
f.c.So(err, ShouldBeNil)
file.WriteString(data)
f.c.So(file.Close(), ShouldBeNil)
}
// mkdir creates an empty directory.
func (f *tempFileSystemImpl) mkdir(rel string) {
f.c.So(os.MkdirAll(f.join(rel), 0777), ShouldBeNil)
}
// symlink creates a symlink.
func (f *tempFileSystemImpl) symlink(oldname, newname string) {
f.c.So(os.Symlink(f.join(oldname), f.join(newname)), ShouldBeNil)
}
// read reads an existing file at a given slash separated path relative to Root().
func (f *tempFileSystemImpl) read(rel string) string {
fd, err := f.OpenFile(f.join(rel))
f.c.So(err, ShouldBeNil)
defer fd.Close()
data, err := ioutil.ReadAll(fd)
f.c.So(err, ShouldBeNil)
return string(data)
}
// readLink reads a symlink at a given slash separated path relative to Root().
func (f *tempFileSystemImpl) readLink(rel string) string {
val, err := os.Readlink(f.join(rel))
f.c.So(err, ShouldBeNil)
return val
}
// isMissing returns true if there's no file at a given slash separated path
// relative to Root().
func (f *tempFileSystemImpl) isMissing(rel string) bool {
_, err := os.Stat(f.join(rel))
return os.IsNotExist(err)
}
// isDir returns true if a file at a given slash separated path relative to
// Root() is a directory.
func (f *tempFileSystemImpl) isDir(rel string) bool {
stat, err := os.Stat(f.join(rel))
if err != nil {
return false
}
return stat.IsDir()
}
// isFile returns true if a file at a given slash separated path relative to
// Root() is a file (not a directory).
func (f *tempFileSystemImpl) isFile(rel string) bool {
stat, err := os.Stat(f.join(rel))
if err != nil {
return false
}
return !stat.IsDir()
}