oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1package docs
2
3import (
4 "sync"
5 "testing"
6 "time"
7)
8
9// recv waits briefly for a seq on ch. The wait exists because a
10// broadcast and its delivery are not the same instant; it is not a
11// synchronisation mechanism the code under test relies on, only a
12// bound on how long a test is willing to be wrong.
13func recv(t *testing.T, ch <-chan int64) (int64, bool) {
14 t.Helper()
15 select {
16 case seq := <-ch:
17 return seq, true
18 case <-time.After(time.Second):
19 return 0, false
20 }
21}
22
23// quiet asserts nothing arrives. Shorter than recv's timeout because
24// a false negative here costs a slow test and a false positive costs
25// a wrong belief about isolation.
26func quiet(t *testing.T, ch <-chan int64) bool {
27 t.Helper()
28 select {
29 case seq := <-ch:
30 t.Errorf("received seq %d on a channel that should have been quiet", seq)
31 return false
32 case <-time.After(50 * time.Millisecond):
33 return true
34 }
35}
36
37func TestHubDeliversToItsOwnDocumentOnly(t *testing.T) {
38 h := newHub()
39
40 mine, stop := h.subscribe(1)
41 defer stop()
42 theirs, stopTheirs := h.subscribe(2)
43 defer stopTheirs()
44
45 h.broadcast(1, 7)
46
47 if seq, ok := recv(t, mine); !ok || seq != 7 {
48 t.Errorf("subscriber to document 1 got (%d, %v), want (7, true)", seq, ok)
49 }
50 quiet(t, theirs)
51}
52
53// A broadcast with nobody listening is not an error and not a leak.
54func TestHubBroadcastWithNoSubscribers(t *testing.T) {
55 h := newHub()
56 h.broadcast(42, 1) // must not panic or block
57
58 ch, stop := h.subscribe(42)
59 defer stop()
60 quiet(t, ch) // and must not have been queued up for a later arrival
61}
62
63// Unsubscribing stops delivery, and doing it twice is safe: the SSE
64// handler defers its stop and may also stop early on a write failure.
65func TestHubUnsubscribeStopsDeliveryAndIsIdempotent(t *testing.T) {
66 h := newHub()
67 ch, stop := h.subscribe(1)
68
69 stop()
70 stop()
71
72 h.broadcast(1, 3)
73 quiet(t, ch)
74}
75
76// TestHubSlowSubscriberNeverBlocksABroadcaster is the property the
77// whole design rests on: broadcast runs on the HTTP request goroutine
78// that just accepted an update, so a subscriber that is not reading —
79// a browser tab on a wedged connection, a handler between select
80// turns — must never be able to hold that request open. The slot is
81// one deep and sends are non-blocking, which is safe precisely
82// because the seq is advisory: a client that misses a notification
83// still fetches ?since=cursor and gets everything.
84func TestHubSlowSubscriberNeverBlocksABroadcaster(t *testing.T) {
85 h := newHub()
86 ch, stop := h.subscribe(1)
87 defer stop()
88
89 done := make(chan struct{})
90 go func() {
91 defer close(done)
92 for seq := int64(1); seq <= 100; seq++ {
93 h.broadcast(1, seq)
94 }
95 }()
96
97 select {
98 case <-done:
99 case <-time.After(time.Second):
100 t.Fatal("broadcast blocked on a subscriber that was not reading")
101 }
102
103 // The subscriber missed most of them, which is fine and is the
104 // point — but it must still be told that something happened, or
105 // it would never fetch.
106 if _, ok := recv(t, ch); !ok {
107 t.Error("a subscriber that fell behind was left with no pending notification at all")
108 }
109}
110
111// The map must not grow forever: a document with no subscribers left
112// holds no entry.
113func TestHubForgetsDocumentsWithNoSubscribers(t *testing.T) {
114 h := newHub()
115
116 _, stopA := h.subscribe(1)
117 _, stopB := h.subscribe(1)
118
119 stopA()
120 if got := h.documentCount(); got != 1 {
121 t.Errorf("after one of two subscribers left, hub tracks %d documents, want 1", got)
122 }
123 stopB()
124 if got := h.documentCount(); got != 0 {
125 t.Errorf("after the last subscriber left, hub tracks %d documents, want 0", got)
126 }
127}
128
129// Run with -race, this is the test that catches a missing lock.
130func TestHubUnderConcurrentUse(t *testing.T) {
131 h := newHub()
132
133 var wg sync.WaitGroup
134 for i := 0; i < 8; i++ {
135 wg.Add(1)
136 go func(n int) {
137 defer wg.Done()
138 for j := 0; j < 50; j++ {
139 ch, stop := h.subscribe(int64(n % 3))
140 h.broadcast(int64(n%3), int64(j))
141 select {
142 case <-ch:
143 default:
144 }
145 stop()
146 }
147 }(i)
148 }
149 wg.Wait()
150
151 if got := h.documentCount(); got != 0 {
152 t.Errorf("hub tracks %d documents after every subscriber left, want 0", got)
153 }
154}
155