// 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 // +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) } } // 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 := port.GetOne(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 = port.GetOne(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 = port.GetOne(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) }