package syncapi_test import ( "sync" "testing" "time" "github.com/dariuszjaniak/warp/server/internal/syncapi" ) // The notifier is the one piece of this component that several goroutines // touch at once, so it is tested on its own rather than only through a // blocked /sync. // woken reports whether a listener has been signalled, without blocking. func woken(l *syncapi.Listener) bool { select { case <-l.Wait(): return true default: return false } } func TestNotifierWakesOnlyWhoIsConcerned(t *testing.T) { n := syncapi.NewNotifier() inRoom := n.Listen("@alice:warp.example", []string{"!a:warp.example"}) elsewhere := n.Listen("@bob:warp.example", []string{"!b:warp.example"}) t.Cleanup(inRoom.Close) t.Cleanup(elsewhere.Close) n.NotifyRoom("!a:warp.example") if !woken(inRoom) { t.Error("a listener on the room was not woken") } if woken(elsewhere) { t.Error("a listener on another room was woken") } } func TestNotifierWakesByUser(t *testing.T) { n := syncapi.NewNotifier() // A user who is in no rooms at all still has to be reachable: an // invite, a to-device message and account data all arrive without any // room they are listening to. listener := n.Listen("@alice:warp.example", nil) t.Cleanup(listener.Close) n.NotifyUser("@alice:warp.example") if !woken(listener) { t.Error("a listener was not woken by a notification addressed to its user") } } func TestNotifierCoalescesWakeups(t *testing.T) { n := syncapi.NewNotifier() listener := n.Listen("@alice:warp.example", []string{"!a:warp.example"}) t.Cleanup(listener.Close) // A listener re-reads everything when it wakes, so two wake-ups and // one are the same instruction. What matters is that the second does // not block the notifier. for range 100 { n.NotifyRoom("!a:warp.example") } if !woken(listener) { t.Fatal("the listener was not woken at all") } if woken(listener) { t.Error("wake-ups queued up instead of coalescing") } } func TestNotifierForgetsAClosedListener(t *testing.T) { n := syncapi.NewNotifier() listener := n.Listen("@alice:warp.example", []string{"!a:warp.example"}) if n.Listeners() != 1 { t.Fatalf("Listeners() = %d, want 1", n.Listeners()) } listener.Close() if n.Listeners() != 0 { t.Errorf("Listeners() = %d after Close, want 0", n.Listeners()) } // Closing twice must not panic or double-remove: a sync closes its // listener on every path out, and some of those paths overlap. listener.Close() // Nothing is left behind to be woken, and notifying an unknown room is // not an error. n.NotifyRoom("!a:warp.example") n.NotifyUser("@alice:warp.example") } func TestNotifierUnderConcurrentUse(t *testing.T) { n := syncapi.NewNotifier() // Listeners coming and going while notifications arrive is the normal // state of a server: every /sync registers, reads and deregisters. // Run under `go test -race` this is the test that would catch a lock // held in the wrong place. var wg sync.WaitGroup stop := make(chan struct{}) for i := range 8 { wg.Add(1) go func(i int) { defer wg.Done() for { select { case <-stop: return default: } l := n.Listen("@alice:warp.example", []string{"!a:warp.example"}) _ = woken(l) l.Close() } }(i) } for range 4 { wg.Add(1) go func() { defer wg.Done() for { select { case <-stop: return default: } n.NotifyRoom("!a:warp.example") n.NotifyUser("@alice:warp.example") n.NotifyUsers([]string{"@alice:warp.example", "@bob:warp.example"}) } }() } time.Sleep(50 * time.Millisecond) close(stop) wg.Wait() if n.Listeners() != 0 { t.Errorf("Listeners() = %d after everything closed, want 0", n.Listeners()) } }