forked from golang/sys
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsyscall_internal_solaris_test.go
269 lines (256 loc) · 8.75 KB
/
syscall_internal_solaris_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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
// Copyright 2022 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.
//go:build solaris
package unix
import (
"fmt"
"os"
"runtime"
"testing"
)
func (e *EventPort) checkInternals(t *testing.T, fds, paths, cookies, pending int) {
t.Helper()
p, err := e.Pending()
if err != nil {
t.Fatalf("failed to query how many events are pending")
}
if len(e.fds) != fds || len(e.paths) != paths || len(e.cookies) != cookies || p != pending {
format := "| fds: %d | paths: %d | cookies: %d | pending: %d |"
expected := fmt.Sprintf(format, fds, paths, cookies, pending)
got := fmt.Sprintf(format, len(e.fds), len(e.paths), len(e.cookies), p)
t.Errorf("Internal state mismatch\nfound: %s\nexpected: %s", got, expected)
}
}
// getOneRetry wraps EventPort.GetOne which in turn wraps a syscall that can be
// interrupted causing us to receive EINTR.
// To prevent our tests from flaking, we retry the syscall until it works
// rather than get unexpected results in our tests.
func getOneRetry(t *testing.T, p *EventPort, timeout *Timespec) (e *PortEvent, err error) {
t.Helper()
for {
e, err = p.GetOne(timeout)
if err != EINTR {
break
}
}
return e, err
}
// getRetry wraps EventPort.Get which in turn wraps a syscall that can be
// interrupted causing us to receive EINTR.
// To prevent our tests from flaking, we retry the syscall until it works
// rather than get unexpected results in our tests.
func getRetry(t *testing.T, p *EventPort, s []PortEvent, min int, timeout *Timespec) (n int, err error) {
t.Helper()
for {
n, err = p.Get(s, min, timeout)
if err != EINTR {
break
}
// If we did get EINTR, make sure we got 0 events
if n != 0 {
t.Fatalf("EventPort.Get returned events on EINTR.\ngot: %d\nexpected: 0", n)
}
}
return n, err
}
// Regression test for DissociatePath returning ENOENT
// This test is intended to create a linear worst
// case scenario of events being associated and
// fired but not consumed before additional
// calls to dissociate and associate happen
// This needs to be an internal test so that
// we can validate the state of the private maps
func TestEventPortDissociateAlreadyGone(t *testing.T) {
port, err := NewEventPort()
if err != nil {
t.Fatalf("failed to create an EventPort")
}
defer port.Close()
dir := t.TempDir()
tmpfile, err := os.CreateTemp(dir, "eventport")
if err != nil {
t.Fatalf("unable to create a tempfile: %v", err)
}
path := tmpfile.Name()
stat, err := os.Stat(path)
if err != nil {
t.Fatalf("unexpected failure to Stat file: %v", err)
}
err = port.AssociatePath(path, stat, FILE_MODIFIED, "cookie1")
if err != nil {
t.Fatalf("unexpected failure associating file: %v", err)
}
// We should have 1 path registered and 1 cookie in the jar
port.checkInternals(t, 0, 1, 1, 0)
// The path is associated, let's delete it.
err = os.Remove(path)
if err != nil {
t.Fatalf("unexpected failure deleting file: %v", err)
}
// The file has been deleted, some sort of pending event is probably
// queued in the kernel waiting for us to get it AND the kernel is
// no longer watching for events on it. BUT... Because we haven't
// consumed the event, this API thinks it's still watched:
watched := port.PathIsWatched(path)
if !watched {
t.Errorf("unexpected result from PathIsWatched")
}
// Ok, let's dissociate the file even before reading the event.
// Oh, ENOENT. I guess it's not associated any more
err = port.DissociatePath(path)
if err != ENOENT {
t.Errorf("unexpected result dissociating a seemingly associated path we know isn't: %v", err)
}
// As established by the return value above, this should clearly be false now:
// Narrator voice: in the first version of this API, it wasn't.
watched = port.PathIsWatched(path)
if watched {
t.Errorf("definitively unwatched file still in the map")
}
// We should have nothing registered, but 1 pending event corresponding
// to the cookie in the jar
port.checkInternals(t, 0, 0, 1, 1)
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
t.Fatalf("creating test file failed: %s", err)
}
err = f.Close()
if err != nil {
t.Fatalf("unexpected failure closing file: %v", err)
}
stat, err = os.Stat(path)
if err != nil {
t.Fatalf("unexpected failure to Stat file: %v", err)
}
c := "cookie2" // c is for cookie, that's good enough for me
err = port.AssociatePath(path, stat, FILE_MODIFIED, c)
if err != nil {
t.Errorf("unexpected failure associating file: %v", err)
}
// We should have 1 registered path and its cookie
// as well as a second cookie corresponding to the pending event
port.checkInternals(t, 0, 1, 2, 1)
// Fire another event
err = os.Remove(path)
if err != nil {
t.Fatalf("unexpected failure deleting file: %v", err)
}
port.checkInternals(t, 0, 1, 2, 2)
err = port.DissociatePath(path)
if err != ENOENT {
t.Errorf("unexpected result dissociating a seemingly associated path we know isn't: %v", err)
}
// By dissociating this path after deletion we ensure that the paths map is now empty
// If we're not careful we could trigger a nil pointer exception
port.checkInternals(t, 0, 0, 2, 2)
f, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
t.Fatalf("creating test file failed: %s", err)
}
err = f.Close()
if err != nil {
t.Fatalf("unexpected failure closing file: %v", err)
}
stat, err = os.Stat(path)
if err != nil {
t.Fatalf("unexpected failure to Stat file: %v", err)
}
// Put a seemingly duplicate cookie in the jar to see if we can trigger an incorrect removal from the paths map
err = port.AssociatePath(path, stat, FILE_MODIFIED, c)
if err != nil {
t.Errorf("unexpected failure associating file: %v", err)
}
port.checkInternals(t, 0, 1, 3, 2)
// run the garbage collector so that if we messed up it should be painfully clear
runtime.GC()
// Before the fix, this would cause a nil pointer exception
e, err := getOneRetry(t, port, nil)
if err != nil {
t.Errorf("failed to get an event: %v", err)
}
port.checkInternals(t, 0, 1, 2, 1)
if e.Cookie != "cookie1" {
t.Errorf(`expected "cookie1", got "%v"`, e.Cookie)
}
// Make sure that a cookie of the same value doesn't cause removal from the paths map incorrectly
e, err = getOneRetry(t, port, nil)
if err != nil {
t.Errorf("failed to get an event: %v", err)
}
port.checkInternals(t, 0, 1, 1, 0)
if e.Cookie != "cookie2" {
t.Errorf(`expected "cookie2", got "%v"`, e.Cookie)
}
err = os.Remove(path)
if err != nil {
t.Fatalf("unexpected failure deleting file: %v", err)
}
// Event has fired, but until processed it should still be in the map
port.checkInternals(t, 0, 1, 1, 1)
e, err = getOneRetry(t, port, nil)
if err != nil {
t.Errorf("failed to get an event: %v", err)
}
if e.Cookie != "cookie2" {
t.Errorf(`expected "cookie2", got "%v"`, e.Cookie)
}
// The maps should be empty and there should be no pending events
port.checkInternals(t, 0, 0, 0, 0)
}
// Regression test for spuriously triggering a panic about memory mismanagement
// that can be triggered by an event processing thread trying to process an event
// after a different thread has already called port.Close().
// Implemented as an internal test so that we can just simulate the Close()
// because if you call close first in the same thread, things work properly
// anyway.
func TestEventPortGetAfterClose(t *testing.T) {
port, err := NewEventPort()
if err != nil {
t.Fatalf("NewEventPort failed: %v", err)
}
// Create, associate, and delete 2 files
for i := 0; i < 2; i++ {
tmpfile, err := os.CreateTemp("", "eventport")
if err != nil {
t.Fatalf("unable to create tempfile: %v", err)
}
path := tmpfile.Name()
stat, err := os.Stat(path)
if err != nil {
t.Fatalf("unable to stat tempfile: %v", err)
}
err = port.AssociatePath(path, stat, FILE_MODIFIED, nil)
if err != nil {
t.Fatalf("unable to AssociatePath tempfile: %v", err)
}
err = os.Remove(path)
if err != nil {
t.Fatalf("unable to Remove tempfile: %v", err)
}
}
n, err := port.Pending()
if err != nil {
t.Errorf("Pending failed: %v", err)
}
if n != 2 {
t.Errorf("expected 2 pending events, got %d", n)
}
// Simulate a close from a different thread
port.fds = nil
port.paths = nil
port.cookies = nil
// Ensure that we get back reasonable errors rather than panic
_, err = getOneRetry(t, port, nil)
if err == nil || err.Error() != "this EventPort is already closed" {
t.Errorf("didn't receive expected error of 'this EventPort is already closed'; got: %v", err)
}
events := make([]PortEvent, 2)
n, err = getRetry(t, port, events, 1, nil)
if n != 0 {
t.Errorf("expected to get back 0 events, got %d", n)
}
if err == nil || err.Error() != "this EventPort is already closed" {
t.Errorf("didn't receive expected error of 'this EventPort is already closed'; got: %v", err)
}
}