1// Copyright 2016 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package fsnotify
6
7import (
8	"os"
9	"path/filepath"
10	"testing"
11	"time"
12
13	"golang.org/x/sys/unix"
14)
15
16// testExchangedataForWatcher tests the watcher with the exchangedata operation on macOS.
17//
18// This is widely used for atomic saves on macOS, e.g. TextMate and in Apple's NSDocument.
19//
20// See https://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man2/exchangedata.2.html
21// Also see: https://github.com/textmate/textmate/blob/cd016be29489eba5f3c09b7b70b06da134dda550/Frameworks/io/src/swap_file_data.cc#L20
22func testExchangedataForWatcher(t *testing.T, watchDir bool) {
23	// Create directory to watch
24	testDir1 := tempMkdir(t)
25
26	// For the intermediate file
27	testDir2 := tempMkdir(t)
28
29	defer os.RemoveAll(testDir1)
30	defer os.RemoveAll(testDir2)
31
32	resolvedFilename := "TestFsnotifyEvents.file"
33
34	// TextMate does:
35	//
36	// 1. exchangedata (intermediate, resolved)
37	// 2. unlink intermediate
38	//
39	// Let's try to simulate that:
40	resolved := filepath.Join(testDir1, resolvedFilename)
41	intermediate := filepath.Join(testDir2, resolvedFilename+"~")
42
43	// Make sure we create the file before we start watching
44	createAndSyncFile(t, resolved)
45
46	watcher := newWatcher(t)
47
48	// Test both variants in isolation
49	if watchDir {
50		addWatch(t, watcher, testDir1)
51	} else {
52		addWatch(t, watcher, resolved)
53	}
54
55	// Receive errors on the error channel on a separate goroutine
56	go func() {
57		for err := range watcher.Errors {
58			t.Fatalf("error received: %s", err)
59		}
60	}()
61
62	// Receive events on the event channel on a separate goroutine
63	eventstream := watcher.Events
64	var removeReceived counter
65	var createReceived counter
66
67	done := make(chan bool)
68
69	go func() {
70		for event := range eventstream {
71			// Only count relevant events
72			if event.Name == filepath.Clean(resolved) {
73				if event.Op&Remove == Remove {
74					removeReceived.increment()
75				}
76				if event.Op&Create == Create {
77					createReceived.increment()
78				}
79			}
80			t.Logf("event received: %s", event)
81		}
82		done <- true
83	}()
84
85	// Repeat to make sure the watched file/directory "survives" the REMOVE/CREATE loop.
86	for i := 1; i <= 3; i++ {
87		// The intermediate file is created in a folder outside the watcher
88		createAndSyncFile(t, intermediate)
89
90		// 1. Swap
91		if err := unix.Exchangedata(intermediate, resolved, 0); err != nil {
92			t.Fatalf("[%d] exchangedata failed: %s", i, err)
93		}
94
95		time.Sleep(50 * time.Millisecond)
96
97		// 2. Delete the intermediate file
98		err := os.Remove(intermediate)
99
100		if err != nil {
101			t.Fatalf("[%d] remove %s failed: %s", i, intermediate, err)
102		}
103
104		time.Sleep(50 * time.Millisecond)
105
106	}
107
108	// We expect this event to be received almost immediately, but let's wait 500 ms to be sure
109	time.Sleep(500 * time.Millisecond)
110
111	// The events will be (CHMOD + REMOVE + CREATE) X 2. Let's focus on the last two:
112	if removeReceived.value() < 3 {
113		t.Fatal("fsnotify remove events have not been received after 500 ms")
114	}
115
116	if createReceived.value() < 3 {
117		t.Fatal("fsnotify create events have not been received after 500 ms")
118	}
119
120	watcher.Close()
121	t.Log("waiting for the event channel to become closed...")
122	select {
123	case <-done:
124		t.Log("event channel closed")
125	case <-time.After(2 * time.Second):
126		t.Fatal("event stream was not closed after 2 seconds")
127	}
128}
129
130// TestExchangedataInWatchedDir test exchangedata operation on file in watched dir.
131func TestExchangedataInWatchedDir(t *testing.T) {
132	testExchangedataForWatcher(t, true)
133}
134
135// TestExchangedataInWatchedDir test exchangedata operation on watched file.
136func TestExchangedataInWatchedFile(t *testing.T) {
137	testExchangedataForWatcher(t, false)
138}
139
140func createAndSyncFile(t *testing.T, filepath string) {
141	f1, err := os.OpenFile(filepath, os.O_WRONLY|os.O_CREATE, 0666)
142	if err != nil {
143		t.Fatalf("creating %s failed: %s", filepath, err)
144	}
145	f1.Sync()
146	f1.Close()
147}
148