tinyspawn_test.go raw
1 package tinyspawn_test
2
3 import (
4 "testing"
5 "time"
6
7 "git.smesh.lol/tiny-spawn"
8 )
9
10 func awaitDone(t *testing.T, done <-chan struct{}) {
11 select {
12 case <-done:
13 case <-time.After(2 * time.Second):
14 t.Fatal("timed out waiting for domain to exit")
15 }
16 }
17
18 // --- Lifecycle ---
19
20 type stopChans struct {
21 Stop tinyspawn.Breaker
22 }
23
24 func stopLoop(_ struct{}, ch stopChans) {
25 <-ch.Stop.Done()
26 }
27
28 func TestSpawnLifecycle(t *testing.T) {
29 stop := tinyspawn.NewBreaker()
30 done := tinyspawn.Spawn(stopLoop, struct{}{}, stopChans{Stop: stop})
31
32 select {
33 case <-done:
34 t.Fatal("domain exited before stop")
35 default:
36 }
37
38 stop.Close()
39 awaitDone(t, done)
40 }
41
42 // --- Data is copied into the domain ---
43
44 type seed struct {
45 N int32
46 }
47
48 type outChans struct {
49 Out chan<- int32
50 }
51
52 func seedLoop(d seed, ch outChans) {
53 ch.Out <- d.N
54 }
55
56 func TestSpawnCopiesData(t *testing.T) {
57 out := make(chan int32)
58 d := seed{N: 1}
59 done := tinyspawn.Spawn(seedLoop, d, outChans{Out: out})
60 d.N = 2
61
62 select {
63 case got := <-out:
64 if got != 1 {
65 t.Fatalf("expected copied value 1, got %d", got)
66 }
67 case <-time.After(2 * time.Second):
68 t.Fatal("timed out waiting for domain")
69 }
70 awaitDone(t, done)
71 }
72
73 // --- Duplex echo ---
74
75 type echoChans struct {
76 Link tinyspawn.Duplex[string]
77 Stop tinyspawn.Breaker
78 }
79
80 func echoLoop(_ struct{}, ch echoChans) {
81 for {
82 select {
83 case v := <-ch.Link.Recv():
84 ch.Link.Send(v)
85 case <-ch.Stop.Done():
86 return
87 }
88 }
89 }
90
91 func TestDuplexEcho(t *testing.T) {
92 parent, child := tinyspawn.NewDuplex[string]()
93 stop := tinyspawn.NewBreaker()
94 done := tinyspawn.Spawn(echoLoop, struct{}{}, echoChans{Link: child, Stop: stop})
95
96 parent.Send("hello")
97 select {
98 case got := <-parent.Recv():
99 if got != "hello" {
100 t.Fatalf("expected hello, got %q", got)
101 }
102 case <-time.After(2 * time.Second):
103 t.Fatal("timed out waiting for echo")
104 }
105
106 stop.Close()
107 awaitDone(t, done)
108 }
109
110 func TestBufferedDuplexTrySend(t *testing.T) {
111 a, b := tinyspawn.NewBufferedDuplex[int32](1)
112 if !a.TrySend(1) {
113 t.Fatal("first buffered send should succeed")
114 }
115 if a.TrySend(2) {
116 t.Fatal("second buffered send should fail while full")
117 }
118 if got := <-b.Recv(); got != 1 {
119 t.Fatalf("expected 1, got %d", got)
120 }
121 }
122
123 // --- Independent domains with per-peer channels ---
124
125 type scaleData struct {
126 Factor int32
127 }
128
129 type scaleChans struct {
130 Link tinyspawn.Duplex[int32]
131 Stop tinyspawn.Breaker
132 }
133
134 func scaleLoop(d scaleData, ch scaleChans) {
135 for {
136 select {
137 case v := <-ch.Link.Recv():
138 ch.Link.Send(v * d.Factor)
139 case <-ch.Stop.Done():
140 return
141 }
142 }
143 }
144
145 func TestIndependentDomains(t *testing.T) {
146 const n = 4
147
148 clients := make([]tinyspawn.Duplex[int32], n)
149 stops := make([]tinyspawn.Breaker, n)
150 dones := make([]<-chan struct{}, n)
151
152 for i := 0; i < n; i++ {
153 parent, child := tinyspawn.NewDuplex[int32]()
154 stop := tinyspawn.NewBreaker()
155 clients[i] = parent
156 stops[i] = stop
157 dones[i] = tinyspawn.Spawn(scaleLoop, scaleData{Factor: int32(i + 1)}, scaleChans{Link: child, Stop: stop})
158 }
159
160 for i := 0; i < n; i++ {
161 clients[i].Send(10)
162 if got := <-clients[i].Recv(); got != int32(10*(i+1)) {
163 t.Fatalf("domain %d: expected %d, got %d", i, 10*(i+1), got)
164 }
165 }
166
167 for i := 0; i < n; i++ {
168 stops[i].Close()
169 }
170 for i := 0; i < n; i++ {
171 awaitDone(t, dones[i])
172 }
173 }
174
175 // --- A breaker fans out to every holder ---
176
177 func waitLoop(_ struct{}, stop tinyspawn.Breaker) {
178 <-stop.Done()
179 }
180
181 func TestBreakerFanOut(t *testing.T) {
182 stop := tinyspawn.NewBreaker()
183 first := tinyspawn.Spawn(waitLoop, struct{}{}, stop)
184 second := tinyspawn.Spawn(waitLoop, struct{}{}, stop)
185
186 stop.Close()
187 awaitDone(t, first)
188 awaitDone(t, second)
189
190 if !stop.Closed() {
191 t.Fatal("breaker should report closed")
192 }
193 }
194