forked from fsnotify/fsnotify
-
Notifications
You must be signed in to change notification settings - Fork 0
/
integration_darwin_test.go
167 lines (136 loc) · 4.32 KB
/
integration_darwin_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
// 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"
"strconv"
"strings"
"testing"
"time"
"golang.org/x/sys/unix"
)
// darwinVersion returns version os Darwin (17 is macOS 10.13).
func darwinVersion() (int, error) {
s, err := unix.Sysctl("kern.osrelease")
if err != nil {
return 0, err
}
s = strings.Split(s, ".")[0]
return strconv.Atoi(s)
}
// 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) {
osVersion, err := darwinVersion()
if err != nil {
t.Fatal("unable to get Darwin version:", err)
}
if osVersion >= 17 {
t.Skip("Exchangedata is deprecated in macOS 10.13")
}
// 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.Errorf("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()
}