148 lines
3.9 KiB
Go
148 lines
3.9 KiB
Go
|
// Copyright 2016 The Go Authors. All rights reserved.
|
||
|
// Use of this source code is governed by a BSD-style
|
||
|
// license that can be found in the LICENSE file.
|
||
|
|
||
|
package fsnotify
|
||
|
|
||
|
import (
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"testing"
|
||
|
"time"
|
||
|
|
||
|
"golang.org/x/sys/unix"
|
||
|
)
|
||
|
|
||
|
// testExchangedataForWatcher tests the watcher with the exchangedata operation on macOS.
|
||
|
//
|
||
|
// This is widely used for atomic saves on macOS, e.g. TextMate and in Apple's NSDocument.
|
||
|
//
|
||
|
// See https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man2/exchangedata.2.html
|
||
|
// Also see: https://github.com/textmate/textmate/blob/cd016be29489eba5f3c09b7b70b06da134dda550/Frameworks/io/src/swap_file_data.cc#L20
|
||
|
func testExchangedataForWatcher(t *testing.T, watchDir bool) {
|
||
|
// Create directory to watch
|
||
|
testDir1 := tempMkdir(t)
|
||
|
|
||
|
// For the intermediate file
|
||
|
testDir2 := tempMkdir(t)
|
||
|
|
||
|
defer os.RemoveAll(testDir1)
|
||
|
defer os.RemoveAll(testDir2)
|
||
|
|
||
|
resolvedFilename := "TestFsnotifyEvents.file"
|
||
|
|
||
|
// TextMate does:
|
||
|
//
|
||
|
// 1. exchangedata (intermediate, resolved)
|
||
|
// 2. unlink intermediate
|
||
|
//
|
||
|
// Let's try to simulate that:
|
||
|
resolved := filepath.Join(testDir1, resolvedFilename)
|
||
|
intermediate := filepath.Join(testDir2, resolvedFilename+"~")
|
||
|
|
||
|
// Make sure we create the file before we start watching
|
||
|
createAndSyncFile(t, resolved)
|
||
|
|
||
|
watcher := newWatcher(t)
|
||
|
|
||
|
// Test both variants in isolation
|
||
|
if watchDir {
|
||
|
addWatch(t, watcher, testDir1)
|
||
|
} else {
|
||
|
addWatch(t, watcher, resolved)
|
||
|
}
|
||
|
|
||
|
// Receive errors on the error channel on a separate goroutine
|
||
|
go func() {
|
||
|
for err := range watcher.Errors {
|
||
|
t.Fatalf("error received: %s", err)
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
// Receive events on the event channel on a separate goroutine
|
||
|
eventstream := watcher.Events
|
||
|
var removeReceived counter
|
||
|
var createReceived counter
|
||
|
|
||
|
done := make(chan bool)
|
||
|
|
||
|
go func() {
|
||
|
for event := range eventstream {
|
||
|
// Only count relevant events
|
||
|
if event.Name == filepath.Clean(resolved) {
|
||
|
if event.Op&Remove == Remove {
|
||
|
removeReceived.increment()
|
||
|
}
|
||
|
if event.Op&Create == Create {
|
||
|
createReceived.increment()
|
||
|
}
|
||
|
}
|
||
|
t.Logf("event received: %s", event)
|
||
|
}
|
||
|
done <- true
|
||
|
}()
|
||
|
|
||
|
// Repeat to make sure the watched file/directory "survives" the REMOVE/CREATE loop.
|
||
|
for i := 1; i <= 3; i++ {
|
||
|
// The intermediate file is created in a folder outside the watcher
|
||
|
createAndSyncFile(t, intermediate)
|
||
|
|
||
|
// 1. Swap
|
||
|
if err := unix.Exchangedata(intermediate, resolved, 0); err != nil {
|
||
|
t.Fatalf("[%d] exchangedata failed: %s", i, err)
|
||
|
}
|
||
|
|
||
|
time.Sleep(50 * time.Millisecond)
|
||
|
|
||
|
// 2. Delete the intermediate file
|
||
|
err := os.Remove(intermediate)
|
||
|
|
||
|
if err != nil {
|
||
|
t.Fatalf("[%d] remove %s failed: %s", i, intermediate, err)
|
||
|
}
|
||
|
|
||
|
time.Sleep(50 * time.Millisecond)
|
||
|
|
||
|
}
|
||
|
|
||
|
// We expect this event to be received almost immediately, but let's wait 500 ms to be sure
|
||
|
time.Sleep(500 * time.Millisecond)
|
||
|
|
||
|
// The events will be (CHMOD + REMOVE + CREATE) X 2. Let's focus on the last two:
|
||
|
if removeReceived.value() < 3 {
|
||
|
t.Fatal("fsnotify remove events have not been received after 500 ms")
|
||
|
}
|
||
|
|
||
|
if createReceived.value() < 3 {
|
||
|
t.Fatal("fsnotify create events have not been received after 500 ms")
|
||
|
}
|
||
|
|
||
|
watcher.Close()
|
||
|
t.Log("waiting for the event channel to become closed...")
|
||
|
select {
|
||
|
case <-done:
|
||
|
t.Log("event channel closed")
|
||
|
case <-time.After(2 * time.Second):
|
||
|
t.Fatal("event stream was not closed after 2 seconds")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// TestExchangedataInWatchedDir test exchangedata operation on file in watched dir.
|
||
|
func TestExchangedataInWatchedDir(t *testing.T) {
|
||
|
testExchangedataForWatcher(t, true)
|
||
|
}
|
||
|
|
||
|
// TestExchangedataInWatchedDir test exchangedata operation on watched file.
|
||
|
func TestExchangedataInWatchedFile(t *testing.T) {
|
||
|
testExchangedataForWatcher(t, false)
|
||
|
}
|
||
|
|
||
|
func createAndSyncFile(t *testing.T, filepath string) {
|
||
|
f1, err := os.OpenFile(filepath, os.O_WRONLY|os.O_CREATE, 0666)
|
||
|
if err != nil {
|
||
|
t.Fatalf("creating %s failed: %s", filepath, err)
|
||
|
}
|
||
|
f1.Sync()
|
||
|
f1.Close()
|
||
|
}
|