2013-07-21 19:26:05 +00:00
|
|
|
/*
|
|
|
|
Copyright 2013 Google Inc.
|
|
|
|
|
|
|
|
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 (
|
2013-07-22 04:13:16 +00:00
|
|
|
"bytes"
|
2013-07-22 05:46:05 +00:00
|
|
|
"fmt"
|
2013-07-22 02:01:22 +00:00
|
|
|
"io"
|
2013-07-21 19:26:05 +00:00
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"path/filepath"
|
|
|
|
"reflect"
|
|
|
|
"runtime"
|
|
|
|
"sort"
|
2013-07-22 02:01:22 +00:00
|
|
|
"strconv"
|
2013-07-21 19:26:05 +00:00
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"camlistore.org/pkg/test"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
errmu sync.Mutex
|
|
|
|
lasterr error
|
|
|
|
)
|
|
|
|
|
|
|
|
func condSkip(t *testing.T) {
|
|
|
|
errmu.Lock()
|
|
|
|
defer errmu.Unlock()
|
|
|
|
if lasterr != nil {
|
|
|
|
t.Skipf("Skipping test; some other test already failed.")
|
|
|
|
}
|
|
|
|
if runtime.GOOS != "darwin" {
|
|
|
|
t.Skipf("Skipping test on OS %q", runtime.GOOS)
|
|
|
|
}
|
|
|
|
if runtime.GOOS == "darwin" {
|
|
|
|
_, err := os.Stat("/Library/Filesystems/osxfusefs.fs/Support/mount_osxfusefs")
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
test.DependencyErrorOrSkip(t)
|
|
|
|
} else if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-07-22 04:13:16 +00:00
|
|
|
type mountEnv struct {
|
|
|
|
t *testing.T
|
|
|
|
mountPoint string
|
|
|
|
process *os.Process
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *mountEnv) Stat(s *stat) int64 {
|
|
|
|
file := filepath.Join(e.mountPoint, ".camli_fs_stats", s.name)
|
|
|
|
slurp, err := ioutil.ReadFile(file)
|
|
|
|
if err != nil {
|
|
|
|
e.t.Fatal(err)
|
|
|
|
}
|
|
|
|
slurp = bytes.TrimSpace(slurp)
|
|
|
|
v, err := strconv.ParseInt(string(slurp), 10, 64)
|
|
|
|
if err != nil {
|
|
|
|
e.t.Fatalf("unexpected value %q in file %s", slurp, file)
|
|
|
|
}
|
|
|
|
return v
|
|
|
|
}
|
|
|
|
|
|
|
|
func cammountTest(t *testing.T, fn func(env *mountEnv)) {
|
2013-07-22 05:46:05 +00:00
|
|
|
dupLog := io.MultiWriter(os.Stderr, testLog{t})
|
|
|
|
log.SetOutput(dupLog)
|
|
|
|
defer log.SetOutput(os.Stderr)
|
|
|
|
|
2013-07-21 19:26:05 +00:00
|
|
|
w := test.GetWorld(t)
|
|
|
|
mountPoint, err := ioutil.TempDir("", "fs-test-mount")
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
verbose := "false"
|
2013-07-22 02:01:22 +00:00
|
|
|
var stderrDest io.Writer = ioutil.Discard
|
|
|
|
if v, _ := strconv.ParseBool(os.Getenv("VERBOSE_FUSE")); v {
|
2013-07-21 19:26:05 +00:00
|
|
|
verbose = "true"
|
2013-07-22 02:01:22 +00:00
|
|
|
stderrDest = testLog{t}
|
2013-07-21 19:26:05 +00:00
|
|
|
}
|
2013-07-22 02:01:22 +00:00
|
|
|
if v, _ := strconv.ParseBool(os.Getenv("VERBOSE_FUSE_STDERR")); v {
|
|
|
|
stderrDest = io.MultiWriter(stderrDest, os.Stderr)
|
|
|
|
}
|
|
|
|
|
2013-07-21 19:26:05 +00:00
|
|
|
mount := w.Cmd("cammount", "--debug="+verbose, mountPoint)
|
2013-07-22 02:01:22 +00:00
|
|
|
mount.Stderr = stderrDest
|
2013-07-22 04:13:16 +00:00
|
|
|
mount.Env = append(mount.Env, "CAMLI_TRACK_FS_STATS=1")
|
2013-07-22 02:01:22 +00:00
|
|
|
|
2013-07-21 19:26:05 +00:00
|
|
|
stdin, err := mount.StdinPipe()
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
if err := mount.Start(); err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
waitc := make(chan error, 1)
|
|
|
|
go func() { waitc <- mount.Wait() }()
|
|
|
|
defer func() {
|
|
|
|
log.Printf("Sending quit")
|
|
|
|
stdin.Write([]byte("q\n"))
|
|
|
|
select {
|
|
|
|
case <-time.After(5 * time.Second):
|
|
|
|
log.Printf("timeout waiting for cammount to finish")
|
|
|
|
mount.Process.Kill()
|
|
|
|
Unmount(mountPoint)
|
|
|
|
case err := <-waitc:
|
|
|
|
log.Printf("cammount exited: %v", err)
|
|
|
|
}
|
2013-07-22 05:46:05 +00:00
|
|
|
if !test.WaitFor(not(dirToBeFUSE(mountPoint)), 5*time.Second, 1*time.Second) {
|
2013-07-21 19:26:05 +00:00
|
|
|
// It didn't unmount. Try again.
|
|
|
|
Unmount(mountPoint)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2013-07-22 05:46:05 +00:00
|
|
|
if !test.WaitFor(dirToBeFUSE(mountPoint), 5*time.Second, 100*time.Millisecond) {
|
2013-07-21 19:26:05 +00:00
|
|
|
t.Fatalf("error waiting for %s to be mounted", mountPoint)
|
|
|
|
}
|
2013-07-22 04:13:16 +00:00
|
|
|
fn(&mountEnv{
|
|
|
|
t: t,
|
|
|
|
mountPoint: mountPoint,
|
|
|
|
process: mount.Process,
|
|
|
|
})
|
|
|
|
|
2013-07-21 19:26:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestRoot(t *testing.T) {
|
|
|
|
condSkip(t)
|
2013-07-22 04:13:16 +00:00
|
|
|
cammountTest(t, func(env *mountEnv) {
|
|
|
|
f, err := os.Open(env.mountPoint)
|
2013-07-21 19:26:05 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
names, err := f.Readdirnames(-1)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
sort.Strings(names)
|
|
|
|
want := []string{"WELCOME.txt", "date", "recent", "roots", "sha1-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "tag"}
|
|
|
|
if !reflect.DeepEqual(names, want) {
|
|
|
|
t.Errorf("root directory = %q; want %q", names, want)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2013-07-22 02:01:22 +00:00
|
|
|
type testLog struct {
|
|
|
|
t *testing.T
|
|
|
|
}
|
|
|
|
|
|
|
|
func (tl testLog) Write(p []byte) (n int, err error) {
|
|
|
|
tl.t.Log(strings.TrimSpace(string(p)))
|
|
|
|
return len(p), nil
|
|
|
|
}
|
|
|
|
|
2013-07-21 19:26:05 +00:00
|
|
|
func TestMutable(t *testing.T) {
|
|
|
|
condSkip(t)
|
2013-07-22 04:13:16 +00:00
|
|
|
cammountTest(t, func(env *mountEnv) {
|
|
|
|
rootDir := filepath.Join(env.mountPoint, "roots", "r")
|
2013-07-22 16:52:48 +00:00
|
|
|
if err := os.MkdirAll(rootDir, 0755); err != nil {
|
2013-07-21 19:26:05 +00:00
|
|
|
t.Fatalf("Failed to make roots/r dir: %v", err)
|
|
|
|
}
|
|
|
|
fi, err := os.Stat(rootDir)
|
|
|
|
if err != nil || !fi.IsDir() {
|
|
|
|
t.Fatalf("Stat of roots/r dir = %v, %v; want a directory", fi, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
filename := filepath.Join(rootDir, "x")
|
|
|
|
f, err := os.Create(filename)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Create: %v", err)
|
|
|
|
}
|
|
|
|
if err := f.Close(); err != nil {
|
|
|
|
t.Fatalf("Close: %v", err)
|
|
|
|
}
|
|
|
|
fi, err = os.Stat(filename)
|
|
|
|
if err != nil || !fi.Mode().IsRegular() || fi.Size() != 0 {
|
|
|
|
t.Fatalf("Stat of roots/r/x = %v, %v; want a 0-byte regular file", fi, err)
|
|
|
|
}
|
|
|
|
|
2013-07-22 02:01:22 +00:00
|
|
|
for _, str := range []string{"foo, ", "bar\n", "another line.\n"} {
|
|
|
|
f, err = os.OpenFile(filename, os.O_WRONLY|os.O_APPEND, 0644)
|
2013-07-21 19:26:05 +00:00
|
|
|
if err != nil {
|
2013-07-22 02:01:22 +00:00
|
|
|
t.Fatalf("OpenFile: %v", err)
|
2013-07-21 19:26:05 +00:00
|
|
|
}
|
2013-07-22 02:01:22 +00:00
|
|
|
if _, err := f.Write([]byte(str)); err != nil {
|
|
|
|
t.Logf("Error with append: %v", err)
|
|
|
|
t.Fatalf("Error appending %q to %s: %v", str, filename, err)
|
2013-07-21 19:26:05 +00:00
|
|
|
}
|
2013-07-22 02:01:22 +00:00
|
|
|
if err := f.Close(); err != nil {
|
|
|
|
t.Fatal(err)
|
2013-07-21 19:26:05 +00:00
|
|
|
}
|
|
|
|
}
|
2013-07-22 04:13:16 +00:00
|
|
|
ro0 := env.Stat(mutFileOpenRO)
|
2013-07-22 02:01:22 +00:00
|
|
|
slurp, err := ioutil.ReadFile(filename)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
2013-07-22 04:13:16 +00:00
|
|
|
if env.Stat(mutFileOpenRO)-ro0 != 1 {
|
|
|
|
t.Error("Read didn't trigger read-only path optimization.")
|
|
|
|
}
|
|
|
|
|
2013-07-22 02:01:22 +00:00
|
|
|
const want = "foo, bar\nanother line.\n"
|
|
|
|
fi, err = os.Stat(filename)
|
|
|
|
if err != nil || !fi.Mode().IsRegular() || fi.Size() != int64(len(want)) {
|
|
|
|
t.Errorf("Stat of roots/r/x = %v, %v; want a %d byte regular file", fi, len(want), err)
|
|
|
|
}
|
|
|
|
if got := string(slurp); got != want {
|
|
|
|
t.Fatalf("contents = %q; want %q", got, want)
|
|
|
|
}
|
2013-07-21 19:26:05 +00:00
|
|
|
|
|
|
|
// Delete it.
|
|
|
|
if err := os.Remove(filename); err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Gone?
|
|
|
|
if _, err := os.Stat(filename); !os.IsNotExist(err) {
|
|
|
|
t.Fatalf("expected file to be gone; got stat err = %v instead", err)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2013-07-22 16:52:48 +00:00
|
|
|
func brokenTest(t *testing.T) {
|
|
|
|
if v, _ := strconv.ParseBool(os.Getenv("RUN_BROKEN_TESTS")); !v {
|
|
|
|
t.Skipf("Skipping broken tests without RUN_BROKEN_TESTS=1")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-07-22 05:46:05 +00:00
|
|
|
func TestFinderCopy(t *testing.T) {
|
|
|
|
if runtime.GOOS != "darwin" {
|
|
|
|
t.Skipf("Skipping Darwin-specific test.")
|
|
|
|
}
|
|
|
|
condSkip(t)
|
|
|
|
cammountTest(t, func(env *mountEnv) {
|
|
|
|
f, err := ioutil.TempFile("", "finder-copy-file")
|
|
|
|
if err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
defer os.Remove(f.Name())
|
|
|
|
want := []byte("Some data for Finder to copy.")
|
|
|
|
if _, err := f.Write(want); err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
if err := f.Close(); err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
destDir := filepath.Join(env.mountPoint, "roots", "r")
|
2013-07-22 16:52:48 +00:00
|
|
|
if err := os.MkdirAll(destDir, 0755); err != nil {
|
2013-07-22 05:46:05 +00:00
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
cmd := exec.Command("osascript")
|
|
|
|
script := fmt.Sprintf(`
|
|
|
|
tell application "Finder"
|
|
|
|
copy file POSIX file %q to folder POSIX file %q
|
|
|
|
end tell
|
|
|
|
`, f.Name(), destDir)
|
|
|
|
cmd.Stdin = strings.NewReader(script)
|
|
|
|
|
|
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
|
|
t.Fatalf("Error running AppleScript: %v, %s", err, out)
|
|
|
|
} else {
|
|
|
|
t.Logf("AppleScript said: %q", out)
|
|
|
|
}
|
|
|
|
|
|
|
|
destFile := filepath.Join(destDir, filepath.Base(f.Name()))
|
|
|
|
fi, err := os.Stat(destFile)
|
|
|
|
if err != nil {
|
|
|
|
t.Errorf("Stat = %v, %v", fi, err)
|
|
|
|
}
|
2013-07-22 16:52:48 +00:00
|
|
|
if fi.Size() != int64(len(want)) {
|
|
|
|
t.Errorf("Dest stat size = %d; want %d", fi.Size(), len(want))
|
|
|
|
}
|
2013-07-22 05:46:05 +00:00
|
|
|
slurp, err := ioutil.ReadFile(destFile)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("ReadFile: %v", err)
|
|
|
|
}
|
|
|
|
if !bytes.Equal(slurp, want) {
|
|
|
|
t.Errorf("Dest file = %q; want %q", slurp, want)
|
|
|
|
}
|
|
|
|
})
|
2013-07-21 19:26:05 +00:00
|
|
|
}
|
|
|
|
|
2013-07-23 01:20:16 +00:00
|
|
|
func TestTextEdit(t *testing.T) {
|
|
|
|
if runtime.GOOS != "darwin" {
|
|
|
|
t.Skipf("Skipping Darwin-specific test.")
|
|
|
|
}
|
|
|
|
condSkip(t)
|
|
|
|
brokenTest(t)
|
|
|
|
cammountTest(t, func(env *mountEnv) {
|
|
|
|
var (
|
|
|
|
testDir = filepath.Join(env.mountPoint, "roots", "r")
|
|
|
|
testFile = filepath.Join(testDir, "some-text-file.txt")
|
|
|
|
content1 = []byte("Some text content.")
|
|
|
|
content2 = []byte("Some replacement content.")
|
|
|
|
)
|
|
|
|
if err := os.MkdirAll(testDir, 0755); err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
if err := ioutil.WriteFile(testFile, content1, 0644); err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
cmd := exec.Command("osascript")
|
|
|
|
script := fmt.Sprintf(`
|
|
|
|
tell application "TextEdit"
|
|
|
|
activate
|
|
|
|
open POSIX file %q
|
|
|
|
tell front document
|
|
|
|
set paragraph 1 to %q as text
|
|
|
|
save
|
|
|
|
close
|
|
|
|
end tell
|
|
|
|
end tell
|
|
|
|
`, testFile, content2)
|
|
|
|
cmd.Stdin = strings.NewReader(script)
|
|
|
|
|
|
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
|
|
t.Fatalf("Error running AppleScript: %v, %s", err, out)
|
|
|
|
} else {
|
|
|
|
t.Logf("AppleScript said: %q", out)
|
|
|
|
}
|
|
|
|
|
|
|
|
fi, err := os.Stat(testFile)
|
|
|
|
if err != nil {
|
|
|
|
t.Errorf("Stat = %v, %v", fi, err)
|
|
|
|
}
|
|
|
|
if fi.Size() != int64(len(content2)) {
|
|
|
|
t.Errorf("Stat size = %d; want %d", fi.Size(), len(content2))
|
|
|
|
}
|
|
|
|
slurp, err := ioutil.ReadFile(testFile)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("ReadFile: %v", err)
|
|
|
|
}
|
|
|
|
if !bytes.Equal(slurp, content2) {
|
|
|
|
t.Errorf("File = %q; want %q", slurp, content2)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2013-07-21 19:26:05 +00:00
|
|
|
func not(cond func() bool) func() bool {
|
|
|
|
return func() bool {
|
|
|
|
return !cond()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func dirToBeFUSE(dir string) func() bool {
|
|
|
|
return func() bool {
|
|
|
|
out, err := exec.Command("df", dir).CombinedOutput()
|
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if runtime.GOOS == "darwin" {
|
|
|
|
if strings.Contains(string(out), "mount_osxfusefs@") {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|