1//go:build linux
2
3package main
4
5import (
6 "bytes"
7 "errors"
8 "fmt"
9 "net"
10 "os"
11 "os/exec"
12 "path/filepath"
13 "strings"
14 "sync"
15 "syscall"
16 "testing"
17 "time"
18
19 "github.com/bnema/vev/pkg/safedir"
20)
21
22type discardTerminalOutput struct{}
23
24func (discardTerminalOutput) Write(p []byte) (int, error) { return len(p), nil }
25func (discardTerminalOutput) Sync() error { return nil }
26func (discardTerminalOutput) Close() error { return nil }
27
28type fakeFDPTY struct {
29 *fakePTY
30 fd uintptr
31}
32
33func (p *fakeFDPTY) Fd() uintptr { return p.fd }
34
35func TestCLILauncherSetsCanonicalClientGeometryBeforeStart(t *testing.T) {
36 original := setPTYWinsize
37 t.Cleanup(func() { setPTYWinsize = original })
38
39 for _, tt := range []struct {
40 name string
41 setSize func(started string, fd int, cols, rows uint16) error
42 wantErr bool
43 }{
44 {
45 name: "starts after canonical geometry",
46 setSize: func(started string, _ int, cols, rows uint16) error {
47 if _, err := os.Stat(started); !errors.Is(err, os.ErrNotExist) {
48 return fmt.Errorf("client started before geometry was set: %w", err)
49 }
50 if cols != 120 || rows != 40 {
51 return fmt.Errorf("geometry = %dx%d, want 120x40", cols, rows)
52 }
53 return nil
54 },
55 },
56 {
57 name: "winsize failure closes PTY before returning",
58 setSize: func(started string, fd int, _, _ uint16) error {
59 if _, err := os.Stat(started); !errors.Is(err, os.ErrNotExist) {
60 return fmt.Errorf("client started after winsize failure: %w", err)
61 }
62 return errTestWinsize
63 },
64 wantErr: true,
65 },
66 } {
67 t.Run(tt.name, func(t *testing.T) {
68 started := filepath.Join(t.TempDir(), "started")
69 t.Setenv("STARTED", started)
70 bin := filepath.Join(t.TempDir(), "client")
71 if err := os.WriteFile(bin, []byte("#!/bin/sh\ntouch \"$STARTED\"\n"), 0o700); err != nil {
72 t.Fatal(err)
73 }
74 runDir := filepath.Join(t.TempDir(), "run")
75 if err := safedir.EnsurePrivate(runDir); err != nil {
76 t.Fatal(err)
77 }
78 setFD := -1
79 setPTYWinsize = func(fd int, cols, rows uint16) error {
80 setFD = fd
81 return tt.setSize(started, fd, cols, rows)
82 }
83 launcher := &cliLauncher{bin: bin}
84 t.Cleanup(func() {
85 if err := launcher.releaseRuntime(runDir); err != nil {
86 t.Error(err)
87 }
88 })
89 p, err := launcher.Launch(processMapping{Role: "client", TracePath: filepath.Join(runDir, "client.jsonl")}, roleCommand{Args: []string{"test"}})
90 if tt.wantErr {
91 if !errors.Is(err, errTestWinsize) || p != nil {
92 t.Fatalf("Launch() = (%T, %v), want winsize error", p, err)
93 }
94 if setFD < 0 {
95 t.Fatal("winsize setter was not called")
96 }
97 if closeErr := syscall.Close(setFD); !errors.Is(closeErr, syscall.EBADF) {
98 t.Fatalf("winsize failure left PTY fd %d open: close error %v", setFD, closeErr)
99 }
100 return
101 }
102 if err != nil {
103 t.Fatal(err)
104 }
105 if setFD < 0 {
106 t.Fatal("winsize setter was not called before successful launch")
107 }
108 if err := p.Close(); err != nil {
109 t.Fatal(err)
110 }
111 })
112 }
113}
114
115var errTestWinsize = errors.New("test winsize failure")
116
117func TestCLIProcessResize(t *testing.T) {
118 original := setPTYWinsize
119 t.Cleanup(func() { setPTYWinsize = original })
120
121 var gotFD int
122 var gotCols, gotRows uint16
123 setPTYWinsize = func(fd int, cols, rows uint16) error {
124 gotFD, gotCols, gotRows = fd, cols, rows
125 return nil
126 }
127 p := &cliProcess{pty: &fakeFDPTY{fakePTY: &fakePTY{}, fd: 42}}
128 if err := p.Resize(119, 40); err != nil {
129 t.Fatal(err)
130 }
131 if gotFD != 42 || gotCols != 119 || gotRows != 40 {
132 t.Fatalf("winsize setter got fd=%d cols=%d rows=%d", gotFD, gotCols, gotRows)
133 }
134
135 if err := (&cliProcess{pty: &fakePTY{}}).Resize(119, 40); err == nil || !strings.Contains(err.Error(), "file descriptor") {
136 t.Fatalf("Resize without FD error = %v", err)
137 }
138}
139
140func TestCLIProcessCloseDrainsQueuedTerminalOutputBeforeClientExit(t *testing.T) {
141 harnessPTY, clientPTY := net.Pipe()
142 t.Cleanup(func() { _ = clientPTY.Close() })
143 waitErr := make(chan error, 1)
144 secondRead := make(chan struct{})
145 p := &cliProcess{
146 pty: harnessPTY,
147 output: discardTerminalOutput{},
148 chunks: make(chan []byte, 1),
149 done: make(chan struct{}),
150 waitErr: waitErr,
151 waitTimeout: func() <-chan time.Time { return time.After(time.Second) },
152 forceCleanup: func() { waitErr <- nil },
153 }
154 go p.copyTerminal()
155 go func() {
156 _, _ = clientPTY.Write([]byte("first"))
157 _, _ = clientPTY.Write([]byte("second"))
158 close(secondRead)
159 }()
160 select {
161 case <-secondRead:
162 case <-time.After(time.Second):
163 t.Fatal("terminal reader did not queue output")
164 }
165 closed := make(chan error, 1)
166 go func() { closed <- p.Close() }()
167 select {
168 case err := <-closed:
169 if err != nil {
170 t.Fatal(err)
171 }
172 case <-time.After(time.Second):
173 t.Fatal("Close did not drain queued terminal output before waiting for client exit")
174 }
175}
176
177func TestCLIProcessCloseKeepsTerminalDrainActiveUntilClientExit(t *testing.T) {
178 pty, err := os.CreateTemp(t.TempDir(), "pty")
179 if err != nil {
180 t.Fatal(err)
181 }
182 waiting := make(chan struct{})
183 deadline := make(chan time.Time)
184 waitErr := make(chan error)
185 done := make(chan struct{})
186 closed := make(chan error, 1)
187 var waitingOnce sync.Once
188 p := &cliProcess{
189 pty: pty,
190 done: done,
191 waitErr: waitErr,
192 waitTimeout: func() <-chan time.Time {
193 waitingOnce.Do(func() { close(waiting) })
194 return deadline
195 },
196 }
197 go func() { closed <- p.Close() }()
198
199 select {
200 case <-waiting:
201 case <-time.After(time.Second):
202 t.Fatal("Close did not begin waiting for graceful client exit")
203 }
204 select {
205 case <-done:
206 t.Fatal("terminal drain stopped before the client exited")
207 default:
208 }
209 waitErr <- nil
210 select {
211 case err := <-closed:
212 if err != nil {
213 t.Fatal(err)
214 }
215 case <-time.After(time.Second):
216 t.Fatal("Close did not finish after the client exited")
217 }
218 select {
219 case <-done:
220 default:
221 t.Fatal("terminal drain remained active after client exit")
222 }
223}
224
225func TestCLIProcessCloseIsBoundedWhenPublicRoleDoesNotExit(t *testing.T) {
226 deadline := make(chan time.Time)
227 close(deadline)
228 var term, kill int
229 p := &cliProcess{
230 waitErr: make(chan error),
231 waitTimeout: func() <-chan time.Time { return deadline },
232 forceCleanup: func() { term++ },
233 forceKill: func() { kill++ },
234 }
235 if err := p.Close(); err == nil {
236 t.Fatal("stuck public role close succeeded")
237 }
238 if term != 1 || kill != 1 {
239 t.Fatalf("TERM/KILL calls = %d/%d, want 1/1", term, kill)
240 }
241}
242
243func TestCLIProcessCloseEscalatesHungClientAfterGracefulDrainTimeout(t *testing.T) {
244 deadline := make(chan time.Time)
245 close(deadline)
246 var graceful, term, kill int
247 p := &cliProcess{
248 pty: &orderedPTY{close: func() {}},
249 waitErr: make(chan error),
250 waitTimeout: func() <-chan time.Time { return deadline },
251 gracefulShutdown: func() { graceful++ },
252 forceCleanup: func() { term++ },
253 forceKill: func() { kill++ },
254 }
255 if err := p.Close(); err == nil {
256 t.Fatal("hung client close succeeded")
257 }
258 if graceful != 1 || term != 1 || kill != 1 {
259 t.Fatalf("graceful/TERM/KILL calls = %d/%d/%d, want 1/1/1", graceful, term, kill)
260 }
261}
262
263func TestCLIProcessCloseEscalationOutcome(t *testing.T) {
264 tests := []struct {
265 name string
266 reapsAt string // "term" or "kill": which rung's callback reaps the process
267 waitTimeout func() func() <-chan time.Time
268 wantErrSub string // "" means Close must return nil
269 wantTerm int
270 wantKill int
271 }{
272 {
273 // A role reaped during the SIGTERM wait stopped gracefully: no SIGKILL,
274 // and Close must not report an error for a stage that can still flush a
275 // trace. The deadline channel never fires, so only the SIGTERM callback
276 // reaping the process can end the wait.
277 name: "SIGTERM stop is graceful",
278 reapsAt: "term",
279 waitTimeout: func() func() <-chan time.Time {
280 never := make(chan time.Time)
281 return func() <-chan time.Time { return never }
282 },
283 wantErrSub: "",
284 wantTerm: 1,
285 wantKill: 0,
286 },
287 {
288 // A role that only dies to SIGKILL may have truncated its trace, so
289 // Close must surface a named escalation error rather than a silent
290 // success. One ready deadline releases the SIGTERM wait so the ladder
291 // escalates to SIGKILL, whose callback then reaps the process.
292 name: "errors after kill escalation",
293 reapsAt: "kill",
294 waitTimeout: func() func() <-chan time.Time {
295 deadline := make(chan time.Time, 1)
296 deadline <- time.Now()
297 return func() <-chan time.Time { return deadline }
298 },
299 wantErrSub: "killed",
300 wantTerm: 1,
301 wantKill: 1,
302 },
303 }
304 for _, tt := range tests {
305 t.Run(tt.name, func(t *testing.T) {
306 waitErr := make(chan error, 1)
307 var term, kill int
308 p := &cliProcess{
309 waitErr: waitErr,
310 waitTimeout: tt.waitTimeout(),
311 forceCleanup: func() {
312 term++
313 if tt.reapsAt == "term" {
314 waitErr <- nil
315 }
316 },
317 forceKill: func() {
318 kill++
319 if tt.reapsAt == "kill" {
320 waitErr <- nil
321 }
322 },
323 }
324 err := p.Close()
325 if tt.wantErrSub == "" {
326 if err != nil {
327 t.Fatalf("Close() error = %v, want nil", err)
328 }
329 } else if err == nil || !strings.Contains(err.Error(), tt.wantErrSub) {
330 t.Fatalf("Close() error = %v, want an error containing %q", err, tt.wantErrSub)
331 }
332 if term != tt.wantTerm || kill != tt.wantKill {
333 t.Fatalf("TERM/KILL calls = %d/%d, want %d/%d", term, kill, tt.wantTerm, tt.wantKill)
334 }
335 })
336 }
337}
338
339func TestCLIProcessWaitReadyRunsJSONProbe(t *testing.T) {
340 tests := []struct {
341 name string
342 start string
343 wantErr string
344 wantProbe bool
345 }{
346 {name: "daemon started", start: "exit 0", wantProbe: true},
347 {name: "daemon start failed", start: "exit 7", wantErr: "start daemon for broker readiness: exit status 7"},
348 }
349 for _, tt := range tests {
350 t.Run(tt.name, func(t *testing.T) {
351 marker := filepath.Join(t.TempDir(), "probe")
352 probe := exec.Command("sh", "-c", `printf '%s\n' '{"schema":"vev.broker-ready/v1","status":"ready"}'; touch "$1"`, "sh", marker)
353 p := &cliProcess{startCommand: exec.Command("sh", "-c", tt.start), readyCommand: probe, waitErr: make(chan error, 1)}
354 err := p.WaitReady()
355 if tt.wantErr == "" {
356 if err != nil {
357 t.Fatal(err)
358 }
359 } else if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
360 t.Fatalf("WaitReady() = %v, want %q", err, tt.wantErr)
361 }
362 _, statErr := os.Stat(marker)
363 if (statErr == nil) != tt.wantProbe {
364 t.Fatalf("readiness probe ran = %t, want %t (stat: %v)", statErr == nil, tt.wantProbe, statErr)
365 }
366 })
367 }
368}
369
370func TestCLILauncherIsolatesRoleXDGEnvironmentAcrossRepetitions(t *testing.T) {
371 captureDir := t.TempDir()
372 t.Setenv("CAPTURE_DIR", captureDir)
373 t.Setenv("XDG_RUNTIME_DIR", filepath.Join(t.TempDir(), "inherited-runtime"))
374 t.Setenv("XDG_STATE_HOME", filepath.Join(t.TempDir(), "inherited-state"))
375
376 bin := filepath.Join(t.TempDir(), "capture-env")
377 if err := os.WriteFile(bin, []byte("#!/bin/sh\nenv > \"$CAPTURE_DIR/$VEV_PERF_PROCESS_ID\"\n"), 0o700); err != nil {
378 t.Fatal(err)
379 }
380 launcher := &cliLauncher{bin: bin}
381 for run := 1; run <= 3; run++ {
382 runDir := filepath.Join(t.TempDir(), fmt.Sprintf("run-%d", run))
383 if err := safedir.EnsurePrivate(runDir); err != nil {
384 t.Fatal(err)
385 }
386 roles := []string{"daemon", "client"}
387 for _, role := range roles {
388 id := fmt.Sprintf("run-%d-%s", run, role)
389 p, err := launcher.Launch(processMapping{
390 Role: role, TracePath: filepath.Join(runDir, role+".jsonl"), ProcessID: id,
391 }, roleCommand{Args: []string{"test"}})
392 if err != nil {
393 t.Fatalf("run %d %s launch: %v", run, role, err)
394 }
395 // The fixture exits on its own. Synchronize with that exit instead of
396 // letting Close race its SIGTERM against the environment capture.
397 proc, ok := p.(*cliProcess)
398 if !ok {
399 t.Fatalf("run %d %s process type = %T, want *cliProcess", run, role, p)
400 }
401 exitErr := <-proc.waitErr
402 proc.waitErr <- exitErr
403 runtimeDir := launcher.runtimes[runDir]
404 if err := p.Close(); err != nil {
405 t.Fatalf("run %d %s close: %v", run, role, err)
406 }
407
408 env, err := os.ReadFile(filepath.Join(captureDir, id))
409 if err != nil {
410 t.Fatalf("run %d %s capture: %v", run, role, err)
411 }
412 for _, want := range []string{
413 "XDG_RUNTIME_DIR=" + runtimeDir,
414 "XDG_STATE_HOME=" + filepath.Join(runDir, "state"),
415 } {
416 name := strings.SplitN(want, "=", 2)[0]
417 if count := bytes.Count(env, []byte(name+"=")); count != 1 {
418 t.Fatalf("run %d %s %s entries = %d, want exactly one; env:\n%s", run, role, name, count, env)
419 }
420 if !bytes.Contains(env, []byte(want+"\n")) {
421 t.Fatalf("run %d %s missing %q; env:\n%s", run, role, want, env)
422 }
423 }
424 }
425 if err := launcher.releaseRuntime(runDir); err != nil {
426 t.Fatalf("run %d release runtime: %v", run, err)
427 }
428 }
429}
430
431func TestPreparePeerReleasesOwnedRuntimeOnPreparationFailure(t *testing.T) {
432 t.Setenv("TMPDIR", t.TempDir())
433 runDir := filepath.Join(t.TempDir(), "run")
434 if err := safedir.EnsurePrivate(runDir); err != nil {
435 t.Fatal(err)
436 }
437 launcher := &cliLauncher{}
438 _, err := launcher.preparePeer(processMapping{
439 Role: "ssh_stdio_peer",
440 TracePath: filepath.Join(runDir, "peer.jsonl"),
441 }, roleCommand{Args: []string{"invalid"}})
442 if err == nil || !strings.Contains(err.Error(), "unsupported broker mux peer command") {
443 t.Fatalf("preparePeer error = %v", err)
444 }
445 if runtimeDir := launcher.runtimes[runDir]; runtimeDir != "" {
446 t.Fatalf("failed peer preparation leaked runtime directory %q", runtimeDir)
447 }
448 if paths, globErr := filepath.Glob(filepath.Join(os.TempDir(), "vev-harness-runtime-*")); globErr != nil || len(paths) != 0 {
449 t.Fatalf("failed peer preparation left runtime directories %q: %v", paths, globErr)
450 }
451}
452
453func TestCLIProcessCloseUsesPublicDaemonShutdown(t *testing.T) {
454 wait := make(chan error, 1)
455 wait <- nil
456 var shutdown int
457 p := &cliProcess{waitErr: wait, shutdown: func() error { shutdown++; return nil }}
458 if err := p.Close(); err != nil {
459 t.Fatal(err)
460 }
461 if shutdown != 0 {
462 t.Fatalf("shutdown calls = %d, want 0 for an exited daemon", shutdown)
463 }
464
465 wait = make(chan error, 1)
466 p = &cliProcess{waitErr: wait, shutdown: func() error { shutdown++; wait <- nil; return nil }}
467 if err := p.Close(); err != nil {
468 t.Fatal(err)
469 }
470 if shutdown != 1 {
471 t.Fatalf("shutdown calls = %d, want 1", shutdown)
472 }
473}
474
475func TestHarnessCanonicalLocalSmokeRealRoles(t *testing.T) {
476 if testing.Short() {
477 t.Skip("public CLI smoke")
478 }
479 root, err := filepath.Abs(filepath.Join("..", ".."))
480 if err != nil {
481 t.Fatal(err)
482 }
483 bin := filepath.Join(t.TempDir(), "vev")
484 build := exec.Command("go", "build", "-o", bin, "./")
485 build.Dir = root
486 if output, err := build.CombinedOutput(); err != nil {
487 t.Fatalf("build public CLI: %v\n%s", err, output)
488 }
489 m, err := readManifest(filepath.Join(root, "testdata", "perf", "manifest.json"))
490 if err != nil {
491 t.Fatal(err)
492 }
493 if err := validateManifest(m); err != nil {
494 t.Fatal(err)
495 }
496 var local scenario
497 for _, s := range m.Scenarios {
498 if s.Topology == "1x4" && s.Workload == "idle" && s.Transport == "local" {
499 local = s
500 break
501 }
502 }
503 if local.ID == "" {
504 t.Fatal("canonical local scenario missing")
505 }
506 dir := t.TempDir()
507 raw, err := os.OpenFile(filepath.Join(dir, "raw.jsonl"), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
508 if err != nil {
509 t.Fatal(err)
510 }
511 closeTestFile(t, raw)
512 h := defaultHarness()
513 h.clock = &fakeClock{} // injected duration keeps the public 30s contract bounded in test
514 h.launcher = &cliLauncher{bin: bin}
515 result, err := h.runOne(options{vevBin: bin, out: dir, duration: minimumDuration}, m, local, 1, raw)
516 if err != nil {
517 t.Fatal(err)
518 }
519 if result.Samples < minimumInIntervalEventSamples || len(result.Processes) != 2 {
520 t.Fatalf("canonical local smoke result=%+v", result)
521 }
522}
523
524func TestTraceEnvironmentMatchesProcessMapping(t *testing.T) {
525 mapping := processMapping{TracePath: "/tmp/trace.jsonl", ProcessID: "p-01", Scenario: "1x4-idle-local", Run: 3}
526 got := strings.Join(traceEnvironment(mapping), "\n")
527 for _, want := range []string{"VEV_PERF_TRACE=/tmp/trace.jsonl", "VEV_PERF_PROCESS_ID=p-01", "VEV_PERF_SCENARIO=1x4-idle-local", "VEV_PERF_RUN=3"} {
528 if !strings.Contains(got, want) {
529 t.Errorf("trace environment missing %q: %s", want, got)
530 }
531 }
532}
533
534func TestCLIProcessCloseStopsShellBeforeProcessGroupCleanup(t *testing.T) {
535 var order []string
536 waitErr := make(chan error, 1)
537 p := &cliProcess{
538 pty: &orderedPTY{close: func() { order = append(order, "pty_closed") }},
539 output: &orderedOutput{close: func() { order = append(order, "output_closed") }},
540 waitErr: waitErr,
541 gracefulShutdown: func() {
542 order = append(order, "shell_exit")
543 // The client closes and waits for its transport descendant before it
544 // reports its own exit to the launcher.
545 order = append(order, "descendant_receive_end")
546 waitErr <- nil
547 },
548 forceCleanup: func() {
549 t.Fatal("normal shutdown sent SIGTERM to the traced descendant process group")
550 },
551 }
552
553 if err := p.Close(); err != nil {
554 t.Fatal(err)
555 }
556 if !equalStrings(order, []string{"shell_exit", "descendant_receive_end", "pty_closed", "output_closed"}) {
557 t.Fatalf("cleanup order=%q", order)
558 }
559}
560
561func TestCLIProcessWarmupWaitsForDelayedTerminalReadiness(t *testing.T) {
562 allowWrite := make(chan struct{})
563 pty, output := &stagedPTY{allow: allowWrite, writes: make(chan []byte, 1)}, &fakeOutput{}
564 p := &cliProcess{pty: pty, output: output, chunks: make(chan []byte)}
565 input := workloadInput(scenario{ID: "s"}, 1, "warmup")
566 done := make(chan error, 1)
567 go func() { done <- p.Warmup(input) }()
568
569 // This is public client output from the initial application prompt/state,
570 // emitted only after the client has entered raw mode. A premature PTY write
571 // blocks on allowWrite and therefore cannot consume this delayed readiness.
572 go func() {
573 p.chunks <- []byte("shell prompt$ ")
574 close(allowWrite)
575 }()
576 select {
577 case got := <-pty.writes:
578 if !bytes.Equal(got, input) {
579 t.Fatalf("warmup write=%q want %q", got, input)
580 }
581 case <-time.After(time.Second):
582 t.Fatal("warmup did not wait for and then proceed from client readiness")
583 }
584 go func() { p.chunks <- append([]byte("application "), inputMarker(input)...); close(p.chunks) }()
585 if err := <-done; err != nil {
586 t.Fatal(err)
587 }
588 if output.syncs != 0 {
589 t.Fatalf("readiness performed a filesystem sync: syncs=%d", output.syncs)
590 }
591}
592
593func TestCLIProcessRejectsPTYLocalEchoAsFlushEvidence(t *testing.T) {
594 pty, output := &fakePTY{}, &fakeOutput{}
595 p := &cliProcess{pty: pty, output: output, chunks: make(chan []byte, 1)}
596 input := workloadInput(scenario{ID: "s"}, 1, "measured-1")
597 p.chunks <- newPTYLocalEcho(input).expected
598 close(p.chunks)
599 flushed := false
600 if err := p.Measure(input, func() error { return nil }, func() error { flushed = true; return nil }); err == nil {
601 t.Fatal("local PTY echo satisfied terminal flush boundary")
602 }
603 if flushed || output.syncs != 0 {
604 t.Fatalf("local echo stamped a flush: flushed=%t syncs=%d", flushed, output.syncs)
605 }
606}
607
608func TestCLIProcessPairsApplicationOutputWithSuccessfulFlush(t *testing.T) {
609 pty, output := &fakePTY{}, &fakeOutput{}
610 p := &cliProcess{pty: pty, output: output, chunks: make(chan []byte, 1)}
611 input := workloadInput(scenario{ID: "s"}, 1, "measured-1")
612 echo := newPTYLocalEcho(input).expected
613 p.chunks <- append(append([]byte(nil), echo...), append([]byte("application "), inputMarker(input)...)...)
614 order := []string{}
615 if err := p.Measure(input, func() error { order = append(order, "injected"); return nil }, func() error { order = append(order, "flushed"); return nil }); err != nil {
616 t.Fatal(err)
617 }
618 if len(pty.writes) != 1 || output.syncs != 0 || !equalStrings(order, []string{"injected", "flushed"}) {
619 t.Fatalf("writes=%q syncs=%d boundary order=%q", pty.writes, output.syncs, order)
620 }
621}
622
623func TestHarnessUsesPublicRoleCommandsAndPTYWorkloads(t *testing.T) {
624 for _, tc := range []struct {
625 role string
626 want []string
627 }{
628 {"daemon", []string{"_broker-production-serve"}},
629 {"client", []string{"new", "perf-s-001"}},
630 {"ssh_stdio_peer", []string{"_broker-mux-stdio", "--production"}},
631 } {
632 t.Run(tc.role, func(t *testing.T) {
633 got := roleArgs(scenario{ID: "s", Transport: "local"}, processMapping{Role: tc.role, Run: 1})
634 if !equalStrings(got.Args, tc.want) {
635 t.Fatalf("args=%q want %q", got.Args, tc.want)
636 }
637 })
638 }
639 input := string(workloadInput(scenario{ID: "s", Workload: "interactive_flood"}, 1, "measured-1"))
640 if string(inputMarker([]byte(input))) != "__VEV_HARNESS_s_r1_measured-1__" || strings.Count(input, "printf 'vev perf output\\n'") != 128 || strings.Contains(input, "while [ $i -lt 128 ]") || !strings.HasSuffix(input, "printf '__VEV_HARNESS_s_r1_measured-1__\\n'\n") {
641 t.Fatalf("workload is not real PTY shell input with an observable marker: %q", input)
642 }
643}