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