1package wayland
2
3import (
4 "context"
5 "sync"
6 "testing"
7 "time"
8
9 "github.com/bnema/neferwl/internal/adapters/logging"
10 "github.com/bnema/neferwl/internal/ports"
11)
12
13// Pushing a capture reply and applying it on the display goroutine allocates
14// nothing in steady state. The count includes the display goroutine:
15// AllocsPerRun reads the global malloc counter.
16func TestCapturedForwardAllocations(t *testing.T) {
17 if raceEnabled {
18 t.Skip("the race detector makes the purego wake callback allocate")
19 }
20 s, _, _, _, _, _ := presentServer(t)
21 done := ports.CaptureDone{ID: 77, Output: "HEADLESS-1", Time: time.Unix(1, 2)}
22 // Posts run in order: once the marker ran, the drain before it did. The
23 // marker is bound once and the channel reused, so waiting allocates nothing.
24 ran := make(chan struct{}, 1)
25 marker := func() { ran <- struct{}{} }
26 for range 4 {
27 s.capturedRing.Push(done)
28 s.display.Post(marker)
29 <-ran
30 }
31 allocs := testing.AllocsPerRun(200, func() {
32 s.capturedRing.Push(done)
33 s.display.Post(marker)
34 <-ran
35 })
36 if allocs != 0 {
37 t.Errorf("push + drain: %.1f allocs, want 0", allocs)
38 }
39}
40
41// A full ring blocks the push until the display goroutine drains it, and no
42// value is lost or reordered.
43func TestPostRingFullBlocksWithoutLoss(t *testing.T) {
44 s, _, _, _, _, _ := presentServer(t)
45 const limit, total = 4, 3*4 + 1
46 var got []int // display goroutine only
47 r := newPostRing(s.display, limit, func(v int) { got = append(got, v) })
48
49 // Stall the display goroutine so nothing drains.
50 release := make(chan struct{})
51 stalled := make(chan struct{})
52 s.display.Post(func() { close(stalled); <-release })
53 <-stalled
54
55 pushed := make(chan int, total)
56 var wg sync.WaitGroup
57 wg.Add(1)
58 go func() {
59 defer wg.Done()
60 for i := range total {
61 if !r.Push(i) {
62 t.Errorf("push %d refused", i)
63 return
64 }
65 pushed <- i
66 }
67 }()
68 for range limit {
69 select {
70 case <-pushed:
71 case <-time.After(2 * time.Second):
72 t.Fatal("pushes below the limit blocked")
73 }
74 }
75 select {
76 case i := <-pushed:
77 t.Fatalf("push %d returned on a full ring", i)
78 case <-time.After(50 * time.Millisecond):
79 }
80 close(release)
81 wg.Wait()
82 // The marker runs after every drain posted before it.
83 var final []int
84 ran := make(chan struct{})
85 for {
86 s.display.Post(func() {
87 if len(got) == total {
88 final = append(final, got...)
89 }
90 ran <- struct{}{}
91 })
92 <-ran
93 if final != nil {
94 break
95 }
96 time.Sleep(time.Millisecond)
97 }
98 for i, v := range final {
99 if v != i {
100 t.Fatalf("applied %v, want 0..%d in order", final, total-1)
101 }
102 }
103}
104
105// A push blocked on a full ring resumes once the display drains it.
106func TestPostRingBlockedPushResumes(t *testing.T) {
107 s, _, _, _, _, _ := presentServer(t)
108 r := newPostRing(s.display, 1, func(int) {})
109 release := make(chan struct{})
110 stalled := make(chan struct{})
111 s.display.Post(func() { close(stalled); <-release })
112 <-stalled
113 if !r.Push(1) {
114 t.Fatal("first push refused")
115 }
116 result := make(chan bool, 1)
117 go func() { result <- r.Push(2) }()
118 select {
119 case ok := <-result:
120 t.Fatalf("push on a full ring returned %v", ok)
121 case <-time.After(50 * time.Millisecond):
122 }
123 close(release)
124 select {
125 case ok := <-result:
126 if !ok {
127 t.Fatal("push refused while the display runs")
128 }
129 case <-time.After(2 * time.Second):
130 t.Fatal("blocked push never resumed")
131 }
132}
133
134// A push blocked on a full ring returns false once the display has stopped,
135// instead of waiting for a drain that never comes.
136func TestPostRingPushStopsWithDisplay(t *testing.T) {
137 s, err := New(Options{RuntimeDir: t.TempDir()}, Channels{}, logging.For(context.Background(), "wayland"))
138 if err != nil {
139 t.Fatal(err)
140 }
141 ctx, cancel := context.WithCancel(context.Background())
142 done := make(chan error, 1)
143 go func() { done <- s.Run(ctx) }()
144 cancel()
145 <-done
146 r := newPostRing(s.display, 1, func(int) {})
147 if !r.Push(1) { // room: queued, its drain is dropped by the stopped display
148 t.Fatal("first push refused")
149 }
150 result := make(chan bool, 1)
151 go func() { result <- r.Push(2) }()
152 select {
153 case ok := <-result:
154 if ok {
155 t.Fatal("push on a stopped display reported success")
156 }
157 case <-time.After(2 * time.Second):
158 t.Fatal("push blocked after the display stopped")
159 }
160}