golang.org/x/sys@v0.9.0/unix/syscall_internal_solaris_test.go (about) 1 // Copyright 2022 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 5 //go:build solaris 6 // +build solaris 7 8 package unix 9 10 import ( 11 "fmt" 12 "os" 13 "runtime" 14 "testing" 15 ) 16 17 func (e *EventPort) checkInternals(t *testing.T, fds, paths, cookies, pending int) { 18 t.Helper() 19 p, err := e.Pending() 20 if err != nil { 21 t.Fatalf("failed to query how many events are pending") 22 } 23 if len(e.fds) != fds || len(e.paths) != paths || len(e.cookies) != cookies || p != pending { 24 format := "| fds: %d | paths: %d | cookies: %d | pending: %d |" 25 expected := fmt.Sprintf(format, fds, paths, cookies, pending) 26 got := fmt.Sprintf(format, len(e.fds), len(e.paths), len(e.cookies), p) 27 t.Errorf("Internal state mismatch\nfound: %s\nexpected: %s", got, expected) 28 } 29 } 30 31 // getOneRetry wraps EventPort.GetOne which in turn wraps a syscall that can be 32 // interrupted causing us to receive EINTR. 33 // To prevent our tests from flaking, we retry the syscall until it works 34 // rather than get unexpected results in our tests. 35 func getOneRetry(t *testing.T, p *EventPort, timeout *Timespec) (e *PortEvent, err error) { 36 t.Helper() 37 for { 38 e, err = p.GetOne(timeout) 39 if err != EINTR { 40 break 41 } 42 } 43 return e, err 44 } 45 46 // getRetry wraps EventPort.Get which in turn wraps a syscall that can be 47 // interrupted causing us to receive EINTR. 48 // To prevent our tests from flaking, we retry the syscall until it works 49 // rather than get unexpected results in our tests. 50 func getRetry(t *testing.T, p *EventPort, s []PortEvent, min int, timeout *Timespec) (n int, err error) { 51 t.Helper() 52 for { 53 n, err = p.Get(s, min, timeout) 54 if err != EINTR { 55 break 56 } 57 // If we did get EINTR, make sure we got 0 events 58 if n != 0 { 59 t.Fatalf("EventPort.Get returned events on EINTR.\ngot: %d\nexpected: 0", n) 60 } 61 } 62 return n, err 63 } 64 65 // Regression test for DissociatePath returning ENOENT 66 // This test is intended to create a linear worst 67 // case scenario of events being associated and 68 // fired but not consumed before additional 69 // calls to dissociate and associate happen 70 // This needs to be an internal test so that 71 // we can validate the state of the private maps 72 func TestEventPortDissociateAlreadyGone(t *testing.T) { 73 port, err := NewEventPort() 74 if err != nil { 75 t.Fatalf("failed to create an EventPort") 76 } 77 defer port.Close() 78 dir := t.TempDir() 79 tmpfile, err := os.CreateTemp(dir, "eventport") 80 if err != nil { 81 t.Fatalf("unable to create a tempfile: %v", err) 82 } 83 path := tmpfile.Name() 84 stat, err := os.Stat(path) 85 if err != nil { 86 t.Fatalf("unexpected failure to Stat file: %v", err) 87 } 88 err = port.AssociatePath(path, stat, FILE_MODIFIED, "cookie1") 89 if err != nil { 90 t.Fatalf("unexpected failure associating file: %v", err) 91 } 92 // We should have 1 path registered and 1 cookie in the jar 93 port.checkInternals(t, 0, 1, 1, 0) 94 // The path is associated, let's delete it. 95 err = os.Remove(path) 96 if err != nil { 97 t.Fatalf("unexpected failure deleting file: %v", err) 98 } 99 // The file has been deleted, some sort of pending event is probably 100 // queued in the kernel waiting for us to get it AND the kernel is 101 // no longer watching for events on it. BUT... Because we haven't 102 // consumed the event, this API thinks it's still watched: 103 watched := port.PathIsWatched(path) 104 if !watched { 105 t.Errorf("unexpected result from PathIsWatched") 106 } 107 // Ok, let's dissociate the file even before reading the event. 108 // Oh, ENOENT. I guess it's not associated any more 109 err = port.DissociatePath(path) 110 if err != ENOENT { 111 t.Errorf("unexpected result dissociating a seemingly associated path we know isn't: %v", err) 112 } 113 // As established by the return value above, this should clearly be false now: 114 // Narrator voice: in the first version of this API, it wasn't. 115 watched = port.PathIsWatched(path) 116 if watched { 117 t.Errorf("definitively unwatched file still in the map") 118 } 119 // We should have nothing registered, but 1 pending event corresponding 120 // to the cookie in the jar 121 port.checkInternals(t, 0, 0, 1, 1) 122 f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666) 123 if err != nil { 124 t.Fatalf("creating test file failed: %s", err) 125 } 126 err = f.Close() 127 if err != nil { 128 t.Fatalf("unexpected failure closing file: %v", err) 129 } 130 stat, err = os.Stat(path) 131 if err != nil { 132 t.Fatalf("unexpected failure to Stat file: %v", err) 133 } 134 c := "cookie2" // c is for cookie, that's good enough for me 135 err = port.AssociatePath(path, stat, FILE_MODIFIED, c) 136 if err != nil { 137 t.Errorf("unexpected failure associating file: %v", err) 138 } 139 // We should have 1 registered path and its cookie 140 // as well as a second cookie corresponding to the pending event 141 port.checkInternals(t, 0, 1, 2, 1) 142 143 // Fire another event 144 err = os.Remove(path) 145 if err != nil { 146 t.Fatalf("unexpected failure deleting file: %v", err) 147 } 148 port.checkInternals(t, 0, 1, 2, 2) 149 err = port.DissociatePath(path) 150 if err != ENOENT { 151 t.Errorf("unexpected result dissociating a seemingly associated path we know isn't: %v", err) 152 } 153 // By dissociating this path after deletion we ensure that the paths map is now empty 154 // If we're not careful we could trigger a nil pointer exception 155 port.checkInternals(t, 0, 0, 2, 2) 156 157 f, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666) 158 if err != nil { 159 t.Fatalf("creating test file failed: %s", err) 160 } 161 err = f.Close() 162 if err != nil { 163 t.Fatalf("unexpected failure closing file: %v", err) 164 } 165 stat, err = os.Stat(path) 166 if err != nil { 167 t.Fatalf("unexpected failure to Stat file: %v", err) 168 } 169 // Put a seemingly duplicate cookie in the jar to see if we can trigger an incorrect removal from the paths map 170 err = port.AssociatePath(path, stat, FILE_MODIFIED, c) 171 if err != nil { 172 t.Errorf("unexpected failure associating file: %v", err) 173 } 174 port.checkInternals(t, 0, 1, 3, 2) 175 176 // run the garbage collector so that if we messed up it should be painfully clear 177 runtime.GC() 178 179 // Before the fix, this would cause a nil pointer exception 180 e, err := getOneRetry(t, port, nil) 181 if err != nil { 182 t.Errorf("failed to get an event: %v", err) 183 } 184 port.checkInternals(t, 0, 1, 2, 1) 185 if e.Cookie != "cookie1" { 186 t.Errorf(`expected "cookie1", got "%v"`, e.Cookie) 187 } 188 // Make sure that a cookie of the same value doesn't cause removal from the paths map incorrectly 189 e, err = getOneRetry(t, port, nil) 190 if err != nil { 191 t.Errorf("failed to get an event: %v", err) 192 } 193 port.checkInternals(t, 0, 1, 1, 0) 194 if e.Cookie != "cookie2" { 195 t.Errorf(`expected "cookie2", got "%v"`, e.Cookie) 196 } 197 198 err = os.Remove(path) 199 if err != nil { 200 t.Fatalf("unexpected failure deleting file: %v", err) 201 } 202 // Event has fired, but until processed it should still be in the map 203 port.checkInternals(t, 0, 1, 1, 1) 204 e, err = getOneRetry(t, port, nil) 205 if err != nil { 206 t.Errorf("failed to get an event: %v", err) 207 } 208 if e.Cookie != "cookie2" { 209 t.Errorf(`expected "cookie2", got "%v"`, e.Cookie) 210 } 211 // The maps should be empty and there should be no pending events 212 port.checkInternals(t, 0, 0, 0, 0) 213 } 214 215 // Regression test for spuriously triggering a panic about memory mismanagement 216 // that can be triggered by an event processing thread trying to process an event 217 // after a different thread has already called port.Close(). 218 // Implemented as an internal test so that we can just simulate the Close() 219 // because if you call close first in the same thread, things work properly 220 // anyway. 221 func TestEventPortGetAfterClose(t *testing.T) { 222 port, err := NewEventPort() 223 if err != nil { 224 t.Fatalf("NewEventPort failed: %v", err) 225 } 226 // Create, associate, and delete 2 files 227 for i := 0; i < 2; i++ { 228 tmpfile, err := os.CreateTemp("", "eventport") 229 if err != nil { 230 t.Fatalf("unable to create tempfile: %v", err) 231 } 232 path := tmpfile.Name() 233 stat, err := os.Stat(path) 234 if err != nil { 235 t.Fatalf("unable to stat tempfile: %v", err) 236 } 237 err = port.AssociatePath(path, stat, FILE_MODIFIED, nil) 238 if err != nil { 239 t.Fatalf("unable to AssociatePath tempfile: %v", err) 240 } 241 err = os.Remove(path) 242 if err != nil { 243 t.Fatalf("unable to Remove tempfile: %v", err) 244 } 245 } 246 n, err := port.Pending() 247 if err != nil { 248 t.Errorf("Pending failed: %v", err) 249 } 250 if n != 2 { 251 t.Errorf("expected 2 pending events, got %d", n) 252 } 253 // Simulate a close from a different thread 254 port.fds = nil 255 port.paths = nil 256 port.cookies = nil 257 // Ensure that we get back reasonable errors rather than panic 258 _, err = getOneRetry(t, port, nil) 259 if err == nil || err.Error() != "this EventPort is already closed" { 260 t.Errorf("didn't receive expected error of 'this EventPort is already closed'; got: %v", err) 261 } 262 events := make([]PortEvent, 2) 263 n, err = getRetry(t, port, events, 1, nil) 264 if n != 0 { 265 t.Errorf("expected to get back 0 events, got %d", n) 266 } 267 if err == nil || err.Error() != "this EventPort is already closed" { 268 t.Errorf("didn't receive expected error of 'this EventPort is already closed'; got: %v", err) 269 } 270 }