launcher_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close