1package app
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "os"
8 "path/filepath"
9 "strconv"
10 "sync"
11 "sync/atomic"
12 "syscall"
13 "testing"
14 "time"
15)
16
17const (
18 spawnTestChildFileEnv = "VEV_SPAWN_TEST_CHILD_FILE"
19 spawnTestLauncherFileEnv = "VEV_SPAWN_TEST_LAUNCHER_FILE"
20 spawnTestReleaseFileEnv = "VEV_SPAWN_TEST_RELEASE_FILE"
21 spawnTestTraceFileEnv = "VEV_SPAWN_TEST_TRACE_FILE"
22)
23
24func TestMain(m *testing.M) {
25 // The hidden offline broker commands are re-executed through this test
26 // binary, so a broker test can exercise the real dispatch path in a real
27 // subprocess. The helper env var keeps that interception out of ordinary
28 // runs.
29 if len(os.Args) >= 2 && os.Getenv(brokerHelperEnv) == "1" {
30 switch os.Args[1] {
31 case brokerMuxStdioCommand, brokerMuxQUICBootstrapCommand, brokerMuxQUICProxyCommand,
32 brokerReadyCommand,
33 productionBrokerServeCommand, productionBrokerLauncherCommand, "--daemon":
34 recordBrokerHelperProcess(os.Args[1])
35 if os.Args[1] == productionBrokerLauncherCommand {
36 if record := os.Getenv(brokerLauncherBlockRecordEnv); record != "" {
37 blockBrokerLauncherHelper(record, os.Getenv(brokerLauncherBlockReleaseEnv))
38 }
39 }
40 if err := Run(os.Args[1:]); err != nil {
41 fmt.Fprintln(os.Stderr, err)
42 os.Exit(ExitCode(err))
43 }
44 os.Exit(0)
45 }
46 }
47 if len(os.Args) == 2 {
48 switch os.Args[1] {
49 case "--daemon-launcher":
50 launcherFile := os.Getenv(spawnTestLauncherFileEnv)
51 if launcherFile != "" {
52 if err := writeProcessRecord(launcherFile); err != nil {
53 os.Exit(2)
54 }
55 }
56 if err := Run(os.Args[1:]); err != nil {
57 os.Exit(2)
58 }
59 if releaseFile := os.Getenv(spawnTestReleaseFileEnv); releaseFile != "" {
60 deadline := time.Now().Add(5 * time.Second)
61 for {
62 if _, err := os.Stat(releaseFile); err == nil {
63 break
64 } else if !errors.Is(err, os.ErrNotExist) || !time.Now().Before(deadline) {
65 os.Exit(2)
66 }
67 time.Sleep(10 * time.Millisecond)
68 }
69 }
70 os.Exit(0)
71 case "--daemon":
72 path := os.Getenv(spawnTestChildFileEnv)
73 if path == "" {
74 os.Exit(2)
75 }
76 if tracePath := os.Getenv(spawnTestTraceFileEnv); tracePath != "" {
77 if err := os.WriteFile(tracePath, []byte(os.Getenv("VEV_PERF_TRACE")), 0o600); err != nil {
78 os.Exit(2)
79 }
80 }
81 if err := writeProcessRecord(path); err != nil {
82 os.Exit(2)
83 }
84 for {
85 time.Sleep(time.Hour)
86 }
87 }
88 }
89 os.Exit(m.Run())
90}
91
92func TestRealSpawnWaitsForLauncherExitAndDoesNotPropagateTrace(t *testing.T) {
93 dir := t.TempDir()
94 childFile := filepath.Join(dir, "daemon.pid")
95 launcherFile := filepath.Join(dir, "launcher.pid")
96 releaseFile := filepath.Join(dir, "release-launcher")
97 traceFile := filepath.Join(dir, "daemon.trace-env")
98 t.Setenv(spawnTestChildFileEnv, childFile)
99 t.Setenv(spawnTestLauncherFileEnv, launcherFile)
100 t.Setenv(spawnTestReleaseFileEnv, releaseFile)
101 t.Setenv(spawnTestTraceFileEnv, traceFile)
102 t.Setenv("VEV_PERF_TRACE", "parent-trace.jsonl")
103
104 // Register cleanup before starting either subprocess. The release marker
105 // unblocks a launcher even when setup or an assertion fails, and PID files
106 // let cleanup terminate every helper that reached its acknowledgement.
107 t.Cleanup(func() {
108 _ = os.WriteFile(releaseFile, nil, 0o600)
109 for _, path := range []string{launcherFile, childFile} {
110 if err := terminateProcessFromFile(path); err != nil {
111 t.Errorf("clean up %s: %v", filepath.Base(path), err)
112 }
113 }
114 })
115
116 spawnResult := make(chan error, 1)
117 go func() {
118 spawnResult <- realSpawn()
119 }()
120
121 launcherPID := waitForProcessRecord(t, launcherFile)
122 daemonPID := waitForProcessRecord(t, childFile)
123
124 select {
125 case err := <-spawnResult:
126 t.Fatalf("realSpawn returned while launcher %d was still running: %v", launcherPID, err)
127 default:
128 }
129 if err := syscall.Kill(launcherPID, 0); err != nil {
130 t.Fatalf("launcher exited before release acknowledgement: %v", err)
131 }
132
133 if err := os.WriteFile(releaseFile, nil, 0o600); err != nil {
134 t.Fatalf("release launcher: %v", err)
135 }
136 select {
137 case err := <-spawnResult:
138 if err != nil {
139 t.Fatalf("realSpawn: %v", err)
140 }
141 case <-time.After(2 * time.Second):
142 t.Fatal("realSpawn did not return after launcher release")
143 }
144
145 if err := waitForProcessExit(launcherPID, 2*time.Second); err != nil {
146 t.Fatalf("launcher still exists after realSpawn returned: %v", err)
147 }
148 if err := syscall.Kill(daemonPID, 0); err != nil {
149 t.Fatalf("daemon exited with its launcher: %v", err)
150 }
151 traceEnv, err := os.ReadFile(traceFile)
152 if err != nil {
153 t.Fatalf("read daemon trace environment: %v", err)
154 }
155 if got := string(traceEnv); got != "" {
156 t.Fatalf("daemon inherited VEV_PERF_TRACE=%q", got)
157 }
158}
159
160func waitForProcessRecord(t *testing.T, path string) int {
161 t.Helper()
162 deadline := time.Now().Add(2 * time.Second)
163 for time.Now().Before(deadline) {
164 data, err := os.ReadFile(path)
165 if err == nil {
166 pid, err := strconv.Atoi(string(data))
167 if err != nil {
168 t.Fatalf("parse process record %s: %v", filepath.Base(path), err)
169 }
170 return pid
171 }
172 if !errors.Is(err, os.ErrNotExist) {
173 t.Fatalf("read process record %s: %v", filepath.Base(path), err)
174 }
175 time.Sleep(10 * time.Millisecond)
176 }
177 t.Fatalf("process did not write %s", filepath.Base(path))
178 return 0
179}
180
181func waitForProcessExit(pid int, timeout time.Duration) error {
182 deadline := time.Now().Add(timeout)
183 for time.Now().Before(deadline) {
184 err := syscall.Kill(pid, 0)
185 if errors.Is(err, syscall.ESRCH) {
186 return nil
187 }
188 if err != nil && !errors.Is(err, syscall.EPERM) {
189 return err
190 }
191 time.Sleep(10 * time.Millisecond)
192 }
193 return fmt.Errorf("process %d did not exit within %s", pid, timeout)
194}
195
196func writeProcessRecord(path string) error {
197 tmp := path + ".tmp"
198 if err := os.WriteFile(tmp, []byte(strconv.Itoa(os.Getpid())), 0o600); err != nil {
199 return err
200 }
201 return os.Rename(tmp, path)
202}
203
204// blockBrokerLauncherHelper is a controlled, blocking launcher: it records its
205// PID and waits for the release marker (or a bounded fallback) so a test can
206// hold a launcher open and prove the spawn wait is bounded by its context.
207func blockBrokerLauncherHelper(record, release string) {
208 if err := writeProcessRecord(record); err != nil {
209 os.Exit(2)
210 }
211 deadline := time.Now().Add(30 * time.Second)
212 for time.Now().Before(deadline) {
213 _, statErr := os.Stat(release)
214 if statErr == nil {
215 break
216 }
217 if !errors.Is(statErr, os.ErrNotExist) {
218 os.Exit(2)
219 }
220 time.Sleep(10 * time.Millisecond)
221 }
222 os.Exit(0)
223}
224
225func terminateProcessFromFile(path string) error {
226 data, err := os.ReadFile(path)
227 if errors.Is(err, os.ErrNotExist) {
228 return nil
229 }
230 if err != nil {
231 return err
232 }
233 pid, err := strconv.Atoi(string(data))
234 if err != nil {
235 return err
236 }
237 if err := syscall.Kill(pid, syscall.SIGKILL); err != nil && !errors.Is(err, syscall.ESRCH) {
238 return err
239 }
240 return waitForProcessExit(pid, 2*time.Second)
241}
242
243func TestAcquireSpawnLockSingleWinnerUnderRace(t *testing.T) {
244 dir := filepath.Join(t.TempDir(), "vev")
245 const racers = 16
246
247 var wins atomic.Int32
248 var wg sync.WaitGroup
249 start := make(chan struct{})
250 releases := make(chan func(), racers)
251
252 for range racers {
253 wg.Go(func() {
254 <-start
255 release, acquired, err := acquireSpawnLock(dir)
256 if err != nil {
257 t.Errorf("acquireSpawnLock: %v", err)
258 return
259 }
260 if acquired {
261 wins.Add(1)
262 releases <- release
263 }
264 })
265 }
266
267 close(start)
268 wg.Wait()
269 close(releases)
270
271 if got := wins.Load(); got != 1 {
272 t.Fatalf("expected exactly one winner, got %d", got)
273 }
274 for release := range releases {
275 release()
276 }
277 // After release, the lock directory is gone and can be re-acquired.
278 release, acquired, err := acquireSpawnLock(dir)
279 if err != nil || !acquired {
280 t.Fatalf("re-acquire after release: acquired=%v err=%v", acquired, err)
281 }
282 release()
283}
284
285func TestAcquireSpawnLockTakesOverStaleLock(t *testing.T) {
286 dir := filepath.Join(t.TempDir(), "vev")
287 if err := os.Mkdir(dir, 0o700); err != nil {
288 t.Fatalf("seeding socket dir: %v", err)
289 }
290 lockPath := filepath.Join(dir, spawnLockName)
291 if err := os.Mkdir(lockPath, 0o700); err != nil {
292 t.Fatalf("seeding lock: %v", err)
293 }
294
295 // A fresh lock must NOT be taken over.
296 if _, acquired, err := acquireSpawnLock(dir); err != nil || acquired {
297 t.Fatalf("fresh lock taken over: acquired=%v err=%v", acquired, err)
298 }
299
300 // Backdate the lock beyond the stale threshold; now it is taken over.
301 old := time.Now().Add(-2 * staleLockAge)
302 if err := os.Chtimes(lockPath, old, old); err != nil {
303 t.Fatalf("backdating lock: %v", err)
304 }
305 release, acquired, err := acquireSpawnLock(dir)
306 if err != nil || !acquired {
307 t.Fatalf("stale lock not taken over: acquired=%v err=%v", acquired, err)
308 }
309 release()
310}
311
312func TestRetryAttemptsChecksOperationErrorBeforeDeadline(t *testing.T) {
313 attemptErr := errors.New("attempt failed")
314 calls := 0
315 _, err := retryAttempts(context.Background(), backoffConfig{}, func() (struct{}, bool, error) {
316 calls++
317 return struct{}{}, false, attemptErr
318 })
319 if !errors.Is(err, attemptErr) {
320 t.Fatalf("retryAttempts error = %v, want %v", err, attemptErr)
321 }
322 if calls != 1 {
323 t.Fatalf("attempt called %d times, want 1", calls)
324 }
325}
326
327func TestRetryAttemptsClampsNonPositiveInitialDelay(t *testing.T) {
328 started := time.Now()
329 calls := 0
330 _, err := retryAttempts(context.Background(), backoffConfig{total: time.Second, max: time.Second}, func() (struct{}, bool, error) {
331 calls++
332 return struct{}{}, calls == 2, nil
333 })
334 if err != nil {
335 t.Fatalf("retryAttempts: %v", err)
336 }
337 if elapsed := time.Since(started); elapsed < 500*time.Microsecond {
338 t.Fatalf("retryAttempts retried after %s, want a positive initial delay", elapsed)
339 }
340}