detached be0d908

fix(obs): resolve harness output paths

commit
be0d9080ee17a72aab76bc80ebe7ff8e3d50e232
author
brice · · 2026-07-13 01:54 UTC
parent
51273ff
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +244 -22
M cmd/vev-perf-harness/main.go +128 -22
@@ -197,6 +197,7 @@ type cliLauncher struct {
197197 netemFactory udpNetemFactory
198198 mu sync.Mutex
199199 peers map[string]peerRoute
200+ runtimes map[string]string
200201 }
201202
202203 type peerRoute struct {
@@ -206,7 +207,10 @@ type peerRoute struct {
206207 netem udpNetem
207208 }
208209
209-type preparedPeer struct{ route peerRoute }
210+type preparedPeer struct {
211+ route peerRoute
212+ cleanupRuntime func() error
213+}
210214
211215 func (p preparedPeer) Warmup([]byte) error { return nil }
212216 func (p preparedPeer) Measure([]byte, func() error, func() error) error { return nil }
@@ -230,6 +234,11 @@ func (p preparedPeer) Close() error {
230234 errs = append(errs, err)
231235 }
232236 }
237+ if p.cleanupRuntime != nil {
238+ if err := p.cleanupRuntime(); err != nil {
239+ errs = append(errs, err)
240+ }
241+ }
233242 return errors.Join(errs...)
234243 }
235244
@@ -240,19 +249,20 @@ type terminalOutput interface {
240249 }
241250
242251 type cliProcess struct {
243- cmd *exec.Cmd
244- pty io.ReadWriteCloser
245- output terminalOutput
246- chunks chan []byte
247- done chan struct{}
248- closed sync.Once
249- waitErr chan error
250- waitTimeout func() <-chan time.Time
251- forceCleanup func()
252- forceKill func()
253- shutdown func() error
254- terminalReady bool
255- readyPath string
252+ cmd *exec.Cmd
253+ pty io.ReadWriteCloser
254+ output terminalOutput
255+ chunks chan []byte
256+ done chan struct{}
257+ closed sync.Once
258+ waitErr chan error
259+ waitTimeout func() <-chan time.Time
260+ forceCleanup func()
261+ forceKill func()
262+ shutdown func() error
263+ cleanupRuntime func() error
264+ terminalReady bool
265+ readyPath string
256266 }
257267
258268 func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProcess, error) {
@@ -269,8 +279,18 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces
269279 }
270280 cmd := exec.Command(l.bin, role.Args...)
271281 runDir := filepath.Dir(m.TracePath)
282+ runtimeDir, err := l.runtimeDir(runDir)
283+ if err != nil {
284+ return nil, err
285+ }
286+ // Each role runs in its isolated evidence directory. All paths passed into
287+ // the launcher were resolved at harness startup, so this cwd cannot redirect
288+ // the binary, manifest-derived traces, runtime, or state paths. Runtime is
289+ // kept in a short private directory because Unix socket paths have a small
290+ // platform limit and canonical evidence paths may be deeply nested.
291+ cmd.Dir = runDir
272292 cmd.Env = append(withoutEnv(os.Environ(), "VEV", "VEV_PERF_TRACE", "VEV_PERF_PROCESS_ID", "VEV_PERF_SCENARIO", "VEV_PERF_RUN", "VEV_REMOTE_TRANSPORT"), traceEnvironment(m)...)
273- cmd.Env = append(cmd.Env, "XDG_RUNTIME_DIR="+filepath.Join(runDir, "runtime"),
293+ cmd.Env = append(cmd.Env, "XDG_RUNTIME_DIR="+runtimeDir,
274294 "XDG_STATE_HOME="+filepath.Join(runDir, "state"), "TERM=xterm-256color")
275295 if m.Role == "client" {
276296 l.mu.Lock()
@@ -284,13 +304,13 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces
284304 cmd.Env = append(cmd.Env, "PATH="+runDir+":"+os.Getenv("PATH"), "VEV_REMOTE_TRANSPORT="+mode)
285305 }
286306 }
287- if err := safedir.EnsurePrivate(filepath.Join(runDir, "runtime")); err != nil {
288- return nil, err
289- }
290307 if err := safedir.EnsurePrivate(filepath.Join(runDir, "state")); err != nil {
291308 return nil, err
292309 }
293- p := &cliProcess{cmd: cmd, waitErr: make(chan error, 1), readyPath: filepath.Join(runDir, "runtime", "vev", "daemon.sock")}
310+ p := &cliProcess{cmd: cmd, waitErr: make(chan error, 1), readyPath: filepath.Join(runtimeDir, "vev", "daemon.sock")}
311+ if m.Role == "daemon" {
312+ p.cleanupRuntime = func() error { return l.releaseRuntime(runDir) }
313+ }
294314 if m.Role == "client" {
295315 master, slave, err := openPTY()
296316 if err != nil {
@@ -309,6 +329,9 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces
309329 _ = output.Close()
310330 _ = master.Close()
311331 _ = slave.Close()
332+ if p.cleanupRuntime != nil {
333+ _ = p.cleanupRuntime()
334+ }
312335 return nil, err
313336 }
314337 _ = slave.Close()
@@ -325,6 +348,9 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces
325348 cmd.Stdin, cmd.Stdout, cmd.Stderr = devNull, devNull, devNull
326349 if err := cmd.Start(); err != nil {
327350 _ = devNull.Close()
351+ if p.cleanupRuntime != nil {
352+ _ = p.cleanupRuntime()
353+ }
328354 return nil, err
329355 }
330356 _ = devNull.Close()
@@ -345,6 +371,41 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces
345371 return p, nil
346372 }
347373
374+// runtimeDir returns one short, private runtime directory shared by the roles
375+// of one run. Keeping the Unix socket outside deeply nested evidence output
376+// avoids the platform socket-path limit without weakening run isolation.
377+func (l *cliLauncher) runtimeDir(runDir string) (string, error) {
378+ l.mu.Lock()
379+ defer l.mu.Unlock()
380+ if path := l.runtimes[runDir]; path != "" {
381+ return path, nil
382+ }
383+ path, err := os.MkdirTemp("", "vev-harness-runtime-")
384+ if err != nil {
385+ return "", err
386+ }
387+ if err := safedir.EnsurePrivate(path); err != nil {
388+ _ = os.RemoveAll(path)
389+ return "", err
390+ }
391+ if l.runtimes == nil {
392+ l.runtimes = make(map[string]string)
393+ }
394+ l.runtimes[runDir] = path
395+ return path, nil
396+}
397+
398+func (l *cliLauncher) releaseRuntime(runDir string) error {
399+ l.mu.Lock()
400+ path := l.runtimes[runDir]
401+ delete(l.runtimes, runDir)
402+ l.mu.Unlock()
403+ if path == "" {
404+ return nil
405+ }
406+ return os.RemoveAll(path)
407+}
408+
348409 // preparePeer installs a per-run ssh command seam. The ordinary public client
349410 // still executes its documented remote attach command; its ssh child invokes
350411 // only the parsed public _stdio or _udp-proxy command. This makes the peer
@@ -352,10 +413,21 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces
352413 // separately launched but unused process.
353414 func (l *cliLauncher) preparePeer(m processMapping, role roleCommand) (launchedProcess, error) {
354415 runDir := filepath.Dir(m.TracePath)
416+ l.mu.Lock()
417+ runtimeDir := l.runtimes[runDir]
418+ l.mu.Unlock()
419+ peerOwnsRuntime := runtimeDir == ""
420+ if peerOwnsRuntime {
421+ var err error
422+ runtimeDir, err = l.runtimeDir(runDir)
423+ if err != nil {
424+ return nil, err
425+ }
426+ }
355427 if err := safedir.EnsurePrivate(runDir); err != nil {
356428 return nil, err
357429 }
358- if err := safedir.EnsurePrivate(filepath.Join(runDir, "runtime")); err != nil {
430+ if err := safedir.EnsurePrivate(runtimeDir); err != nil {
359431 return nil, err
360432 }
361433 if err := safedir.EnsurePrivate(filepath.Join(runDir, "state")); err != nil {
@@ -416,7 +488,7 @@ case "$*" in
416488 ;;
417489 *) echo 'vev harness ssh seam rejected non-vev command' >&2; exit 64 ;;
418490 esac
419-`, m.TracePath, m.ProcessID, filepath.Join(runDir, "runtime"), filepath.Join(runDir, "state"), l.bin, role.Args[1], ready, stderr, target, netemPort, route.pidPath, m.Scenario, m.Run)
491+`, m.TracePath, m.ProcessID, runtimeDir, filepath.Join(runDir, "state"), l.bin, role.Args[1], ready, stderr, target, netemPort, route.pidPath, m.Scenario, m.Run)
420492 if err := os.WriteFile(shim, []byte(body), 0o700); err != nil {
421493 if route.netem != nil {
422494 _ = route.netem.Close()
@@ -436,7 +508,11 @@ esac
436508 }
437509 l.peers[runDir] = route
438510 l.mu.Unlock()
439- return preparedPeer{route: route}, nil
511+ peer := preparedPeer{route: route}
512+ if peerOwnsRuntime {
513+ peer.cleanupRuntime = func() error { return l.releaseRuntime(runDir) }
514+ }
515+ return peer, nil
440516 }
441517
442518 func remoteMode(t transport) (string, error) {
@@ -744,6 +820,11 @@ func (p *cliProcess) Close() error {
744820 result = err
745821 }
746822 }
823+ if p.cleanupRuntime != nil {
824+ if err := p.cleanupRuntime(); result == nil {
825+ result = err
826+ }
827+ }
747828 })
748829 return result
749830 }
@@ -873,6 +954,10 @@ func run(args []string, h *harness) error {
873954 if err != nil {
874955 return err
875956 }
957+ opt, err = resolvePathOptions(opt)
958+ if err != nil {
959+ return err
960+ }
876961 m, err := readManifest(opt.manifest)
877962 if err != nil {
878963 return err
@@ -972,6 +1057,27 @@ func parseOptions(args []string) (options, error) {
9721057 }
9731058 return o, nil
9741059 }
1060+
1061+// resolvePathOptions captures the harness cwd once, before role launchers
1062+// assign their isolated working directories. All downstream filesystem access,
1063+// manifests, and process environments consequently refer to the same paths.
1064+func resolvePathOptions(o options) (options, error) {
1065+ cwd, err := os.Getwd()
1066+ if err != nil {
1067+ return o, fmt.Errorf("resolve harness working directory: %w", err)
1068+ }
1069+ absolute := func(path string) string {
1070+ if filepath.IsAbs(path) {
1071+ return filepath.Clean(path)
1072+ }
1073+ return filepath.Clean(filepath.Join(cwd, path))
1074+ }
1075+ o.vevBin = absolute(o.vevBin)
1076+ o.manifest = absolute(o.manifest)
1077+ o.out = absolute(o.out)
1078+ return o, nil
1079+}
1080+
9751081 func readManifest(path string) (manifest, error) {
9761082 f, e := os.Open(path)
9771083 if e != nil {
@@ -197,6 +197,7 @@ type cliLauncher struct {
197 netemFactory udpNetemFactory197 netemFactory udpNetemFactory
198 mu sync.Mutex198 mu sync.Mutex
199 peers map[string]peerRoute199 peers map[string]peerRoute
200+ runtimes map[string]string
200 }201 }
201 202
202 type peerRoute struct {203 type peerRoute struct {
@@ -206,7 +207,10 @@ type peerRoute struct {
206 netem udpNetem207 netem udpNetem
207 }208 }
208 209
209-type preparedPeer struct{ route peerRoute }210+type preparedPeer struct {
211+ route peerRoute
212+ cleanupRuntime func() error
213+}
210 214
211 func (p preparedPeer) Warmup([]byte) error { return nil }215 func (p preparedPeer) Warmup([]byte) error { return nil }
212 func (p preparedPeer) Measure([]byte, func() error, func() error) error { return nil }216 func (p preparedPeer) Measure([]byte, func() error, func() error) error { return nil }
@@ -230,6 +234,11 @@ func (p preparedPeer) Close() error {
230 errs = append(errs, err)234 errs = append(errs, err)
231 }235 }
232 }236 }
237+ if p.cleanupRuntime != nil {
238+ if err := p.cleanupRuntime(); err != nil {
239+ errs = append(errs, err)
240+ }
241+ }
233 return errors.Join(errs...)242 return errors.Join(errs...)
234 }243 }
235 244
@@ -240,19 +249,20 @@ type terminalOutput interface {
240 }249 }
241 250
242 type cliProcess struct {251 type cliProcess struct {
243- cmd *exec.Cmd252+ cmd *exec.Cmd
244- pty io.ReadWriteCloser253+ pty io.ReadWriteCloser
245- output terminalOutput254+ output terminalOutput
246- chunks chan []byte255+ chunks chan []byte
247- done chan struct{}256+ done chan struct{}
248- closed sync.Once257+ closed sync.Once
249- waitErr chan error258+ waitErr chan error
250- waitTimeout func() <-chan time.Time259+ waitTimeout func() <-chan time.Time
251- forceCleanup func()260+ forceCleanup func()
252- forceKill func()261+ forceKill func()
253- shutdown func() error262+ shutdown func() error
254- terminalReady bool263+ cleanupRuntime func() error
255- readyPath string264+ terminalReady bool
265+ readyPath string
256 }266 }
257 267
258 func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProcess, error) {268 func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProcess, error) {
@@ -269,8 +279,18 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces
269 }279 }
270 cmd := exec.Command(l.bin, role.Args...)280 cmd := exec.Command(l.bin, role.Args...)
271 runDir := filepath.Dir(m.TracePath)281 runDir := filepath.Dir(m.TracePath)
282+ runtimeDir, err := l.runtimeDir(runDir)
283+ if err != nil {
284+ return nil, err
285+ }
286+ // Each role runs in its isolated evidence directory. All paths passed into
287+ // the launcher were resolved at harness startup, so this cwd cannot redirect
288+ // the binary, manifest-derived traces, runtime, or state paths. Runtime is
289+ // kept in a short private directory because Unix socket paths have a small
290+ // platform limit and canonical evidence paths may be deeply nested.
291+ cmd.Dir = runDir
272 cmd.Env = append(withoutEnv(os.Environ(), "VEV", "VEV_PERF_TRACE", "VEV_PERF_PROCESS_ID", "VEV_PERF_SCENARIO", "VEV_PERF_RUN", "VEV_REMOTE_TRANSPORT"), traceEnvironment(m)...)292 cmd.Env = append(withoutEnv(os.Environ(), "VEV", "VEV_PERF_TRACE", "VEV_PERF_PROCESS_ID", "VEV_PERF_SCENARIO", "VEV_PERF_RUN", "VEV_REMOTE_TRANSPORT"), traceEnvironment(m)...)
273- cmd.Env = append(cmd.Env, "XDG_RUNTIME_DIR="+filepath.Join(runDir, "runtime"),293+ cmd.Env = append(cmd.Env, "XDG_RUNTIME_DIR="+runtimeDir,
274 "XDG_STATE_HOME="+filepath.Join(runDir, "state"), "TERM=xterm-256color")294 "XDG_STATE_HOME="+filepath.Join(runDir, "state"), "TERM=xterm-256color")
275 if m.Role == "client" {295 if m.Role == "client" {
276 l.mu.Lock()296 l.mu.Lock()
@@ -284,13 +304,13 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces
284 cmd.Env = append(cmd.Env, "PATH="+runDir+":"+os.Getenv("PATH"), "VEV_REMOTE_TRANSPORT="+mode)304 cmd.Env = append(cmd.Env, "PATH="+runDir+":"+os.Getenv("PATH"), "VEV_REMOTE_TRANSPORT="+mode)
285 }305 }
286 }306 }
287- if err := safedir.EnsurePrivate(filepath.Join(runDir, "runtime")); err != nil {
288- return nil, err
289- }
290 if err := safedir.EnsurePrivate(filepath.Join(runDir, "state")); err != nil {307 if err := safedir.EnsurePrivate(filepath.Join(runDir, "state")); err != nil {
291 return nil, err308 return nil, err
292 }309 }
293- p := &cliProcess{cmd: cmd, waitErr: make(chan error, 1), readyPath: filepath.Join(runDir, "runtime", "vev", "daemon.sock")}310+ p := &cliProcess{cmd: cmd, waitErr: make(chan error, 1), readyPath: filepath.Join(runtimeDir, "vev", "daemon.sock")}
311+ if m.Role == "daemon" {
312+ p.cleanupRuntime = func() error { return l.releaseRuntime(runDir) }
313+ }
294 if m.Role == "client" {314 if m.Role == "client" {
295 master, slave, err := openPTY()315 master, slave, err := openPTY()
296 if err != nil {316 if err != nil {
@@ -309,6 +329,9 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces
309 _ = output.Close()329 _ = output.Close()
310 _ = master.Close()330 _ = master.Close()
311 _ = slave.Close()331 _ = slave.Close()
332+ if p.cleanupRuntime != nil {
333+ _ = p.cleanupRuntime()
334+ }
312 return nil, err335 return nil, err
313 }336 }
314 _ = slave.Close()337 _ = slave.Close()
@@ -325,6 +348,9 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces
325 cmd.Stdin, cmd.Stdout, cmd.Stderr = devNull, devNull, devNull348 cmd.Stdin, cmd.Stdout, cmd.Stderr = devNull, devNull, devNull
326 if err := cmd.Start(); err != nil {349 if err := cmd.Start(); err != nil {
327 _ = devNull.Close()350 _ = devNull.Close()
351+ if p.cleanupRuntime != nil {
352+ _ = p.cleanupRuntime()
353+ }
328 return nil, err354 return nil, err
329 }355 }
330 _ = devNull.Close()356 _ = devNull.Close()
@@ -345,6 +371,41 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces
345 return p, nil371 return p, nil
346 }372 }
347 373
374+// runtimeDir returns one short, private runtime directory shared by the roles
375+// of one run. Keeping the Unix socket outside deeply nested evidence output
376+// avoids the platform socket-path limit without weakening run isolation.
377+func (l *cliLauncher) runtimeDir(runDir string) (string, error) {
378+ l.mu.Lock()
379+ defer l.mu.Unlock()
380+ if path := l.runtimes[runDir]; path != "" {
381+ return path, nil
382+ }
383+ path, err := os.MkdirTemp("", "vev-harness-runtime-")
384+ if err != nil {
385+ return "", err
386+ }
387+ if err := safedir.EnsurePrivate(path); err != nil {
388+ _ = os.RemoveAll(path)
389+ return "", err
390+ }
391+ if l.runtimes == nil {
392+ l.runtimes = make(map[string]string)
393+ }
394+ l.runtimes[runDir] = path
395+ return path, nil
396+}
397+
398+func (l *cliLauncher) releaseRuntime(runDir string) error {
399+ l.mu.Lock()
400+ path := l.runtimes[runDir]
401+ delete(l.runtimes, runDir)
402+ l.mu.Unlock()
403+ if path == "" {
404+ return nil
405+ }
406+ return os.RemoveAll(path)
407+}
408+
348 // preparePeer installs a per-run ssh command seam. The ordinary public client409 // preparePeer installs a per-run ssh command seam. The ordinary public client
349 // still executes its documented remote attach command; its ssh child invokes410 // still executes its documented remote attach command; its ssh child invokes
350 // only the parsed public _stdio or _udp-proxy command. This makes the peer411 // only the parsed public _stdio or _udp-proxy command. This makes the peer
@@ -352,10 +413,21 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces
352 // separately launched but unused process.413 // separately launched but unused process.
353 func (l *cliLauncher) preparePeer(m processMapping, role roleCommand) (launchedProcess, error) {414 func (l *cliLauncher) preparePeer(m processMapping, role roleCommand) (launchedProcess, error) {
354 runDir := filepath.Dir(m.TracePath)415 runDir := filepath.Dir(m.TracePath)
416+ l.mu.Lock()
417+ runtimeDir := l.runtimes[runDir]
418+ l.mu.Unlock()
419+ peerOwnsRuntime := runtimeDir == ""
420+ if peerOwnsRuntime {
421+ var err error
422+ runtimeDir, err = l.runtimeDir(runDir)
423+ if err != nil {
424+ return nil, err
425+ }
426+ }
355 if err := safedir.EnsurePrivate(runDir); err != nil {427 if err := safedir.EnsurePrivate(runDir); err != nil {
356 return nil, err428 return nil, err
357 }429 }
358- if err := safedir.EnsurePrivate(filepath.Join(runDir, "runtime")); err != nil {430+ if err := safedir.EnsurePrivate(runtimeDir); err != nil {
359 return nil, err431 return nil, err
360 }432 }
361 if err := safedir.EnsurePrivate(filepath.Join(runDir, "state")); err != nil {433 if err := safedir.EnsurePrivate(filepath.Join(runDir, "state")); err != nil {
@@ -416,7 +488,7 @@ case "$*" in
416 ;;488 ;;
417 *) echo 'vev harness ssh seam rejected non-vev command' >&2; exit 64 ;;489 *) echo 'vev harness ssh seam rejected non-vev command' >&2; exit 64 ;;
418 esac490 esac
419-`, m.TracePath, m.ProcessID, filepath.Join(runDir, "runtime"), filepath.Join(runDir, "state"), l.bin, role.Args[1], ready, stderr, target, netemPort, route.pidPath, m.Scenario, m.Run)491+`, m.TracePath, m.ProcessID, runtimeDir, filepath.Join(runDir, "state"), l.bin, role.Args[1], ready, stderr, target, netemPort, route.pidPath, m.Scenario, m.Run)
420 if err := os.WriteFile(shim, []byte(body), 0o700); err != nil {492 if err := os.WriteFile(shim, []byte(body), 0o700); err != nil {
421 if route.netem != nil {493 if route.netem != nil {
422 _ = route.netem.Close()494 _ = route.netem.Close()
@@ -436,7 +508,11 @@ esac
436 }508 }
437 l.peers[runDir] = route509 l.peers[runDir] = route
438 l.mu.Unlock()510 l.mu.Unlock()
439- return preparedPeer{route: route}, nil511+ peer := preparedPeer{route: route}
512+ if peerOwnsRuntime {
513+ peer.cleanupRuntime = func() error { return l.releaseRuntime(runDir) }
514+ }
515+ return peer, nil
440 }516 }
441 517
442 func remoteMode(t transport) (string, error) {518 func remoteMode(t transport) (string, error) {
@@ -744,6 +820,11 @@ func (p *cliProcess) Close() error {
744 result = err820 result = err
745 }821 }
746 }822 }
823+ if p.cleanupRuntime != nil {
824+ if err := p.cleanupRuntime(); result == nil {
825+ result = err
826+ }
827+ }
747 })828 })
748 return result829 return result
749 }830 }
@@ -873,6 +954,10 @@ func run(args []string, h *harness) error {
873 if err != nil {954 if err != nil {
874 return err955 return err
875 }956 }
957+ opt, err = resolvePathOptions(opt)
958+ if err != nil {
959+ return err
960+ }
876 m, err := readManifest(opt.manifest)961 m, err := readManifest(opt.manifest)
877 if err != nil {962 if err != nil {
878 return err963 return err
@@ -972,6 +1057,27 @@ func parseOptions(args []string) (options, error) {
972 }1057 }
973 return o, nil1058 return o, nil
974 }1059 }
1060+
1061+// resolvePathOptions captures the harness cwd once, before role launchers
1062+// assign their isolated working directories. All downstream filesystem access,
1063+// manifests, and process environments consequently refer to the same paths.
1064+func resolvePathOptions(o options) (options, error) {
1065+ cwd, err := os.Getwd()
1066+ if err != nil {
1067+ return o, fmt.Errorf("resolve harness working directory: %w", err)
1068+ }
1069+ absolute := func(path string) string {
1070+ if filepath.IsAbs(path) {
1071+ return filepath.Clean(path)
1072+ }
1073+ return filepath.Clean(filepath.Join(cwd, path))
1074+ }
1075+ o.vevBin = absolute(o.vevBin)
1076+ o.manifest = absolute(o.manifest)
1077+ o.out = absolute(o.out)
1078+ return o, nil
1079+}
1080+
975 func readManifest(path string) (manifest, error) {1081 func readManifest(path string) (manifest, error) {
976 f, e := os.Open(path)1082 f, e := os.Open(path)
977 if e != nil {1083 if e != nil {
M cmd/vev-perf-harness/main_test.go +116 -0
@@ -292,6 +292,94 @@ func TestHarnessCanonicalLocalRolesAreIsolatedAcrossRepetitions(t *testing.T) {
292292 }
293293 }
294294
295+func TestHarnessResolvesCanonicalRelativePathsBeforeRoleCWDChanges(t *testing.T) {
296+ if testing.Short() {
297+ t.Skip("public CLI relative-path lifecycle")
298+ }
299+ root, err := filepath.Abs(filepath.Join("..", ".."))
300+ if err != nil {
301+ t.Fatal(err)
302+ }
303+ // Keep the real Unix-socket path below its platform limit while testing the
304+ // exact documented relative output hierarchy.
305+ workspace, err := os.MkdirTemp("", "vev-rel-")
306+ if err != nil {
307+ t.Fatal(err)
308+ }
309+ t.Cleanup(func() { _ = os.RemoveAll(workspace) })
310+ bin := filepath.Join(workspace, "vev")
311+ build := exec.Command("/usr/local/go/bin/go", "build", "-o", bin, "./")
312+ build.Dir = root
313+ if output, err := build.CombinedOutput(); err != nil {
314+ t.Fatalf("build public CLI: %v\n%s", err, output)
315+ }
316+ manifestPath := filepath.Join(workspace, "testdata", "perf", "manifest.json")
317+ if err := os.MkdirAll(filepath.Dir(manifestPath), 0o700); err != nil {
318+ t.Fatal(err)
319+ }
320+ contents, err := os.ReadFile(filepath.Join(root, "testdata", "perf", "manifest.json"))
321+ if err != nil {
322+ t.Fatal(err)
323+ }
324+ if err := os.WriteFile(manifestPath, contents, 0o600); err != nil {
325+ t.Fatal(err)
326+ }
327+ originalWD, err := os.Getwd()
328+ if err != nil {
329+ t.Fatal(err)
330+ }
331+ if err := os.Chdir(workspace); err != nil {
332+ t.Fatal(err)
333+ }
334+ t.Cleanup(func() {
335+ if err := os.Chdir(originalWD); err != nil {
336+ t.Errorf("restore cwd: %v", err)
337+ }
338+ })
339+
340+ // These are the documented relative locations. cliLauncher changes every
341+ // role to its run directory, so a failure to resolve them before launch
342+ // would make VEV_PERF_TRACE point at a nonexistent nested directory.
343+ h := defaultHarness()
344+ h.clock = &fakeClock{}
345+ if err := run([]string{
346+ "--vev-bin", "./vev",
347+ "--manifest", "testdata/perf/manifest.json",
348+ "--out", "testdata/perf/results",
349+ "--scenario", "1x4-idle-local",
350+ "--warmup", "0s",
351+ "--duration", "30s",
352+ "--repetitions", "10",
353+ }, h); err != nil {
354+ t.Fatal(err)
355+ }
356+
357+ out := filepath.Join(workspace, "testdata", "perf", "results")
358+ for run := 1; run <= 10; run++ {
359+ runDir := filepath.Join(out, fmt.Sprintf("1x4-idle-local-run-%03d", run))
360+ manifestBytes, err := os.ReadFile(filepath.Join(runDir, "manifest.json"))
361+ if err != nil {
362+ t.Fatalf("run %d manifest: %v", run, err)
363+ }
364+ var persisted runManifest
365+ if err := json.Unmarshal(manifestBytes, &persisted); err != nil {
366+ t.Fatalf("run %d manifest JSON: %v", run, err)
367+ }
368+ if persisted.Run != run || len(persisted.Processes) != 2 {
369+ t.Fatalf("run %d manifest=%+v", run, persisted)
370+ }
371+ for _, process := range persisted.Processes {
372+ if !filepath.IsAbs(process.TracePath) || filepath.Dir(process.TracePath) != runDir {
373+ t.Fatalf("run %d %s trace path=%q, want absolute path in %q", run, process.Role, process.TracePath, runDir)
374+ }
375+ trace, err := os.ReadFile(process.TracePath)
376+ if err != nil || len(bytes.TrimSpace(trace)) == 0 {
377+ t.Fatalf("run %d %s trace=%q err=%v", run, process.Role, process.TracePath, err)
378+ }
379+ }
380+ }
381+}
382+
295383 func TestHarnessScenarioSelectionKeepsCanonicalManifestValidation(t *testing.T) {
296384 root, err := filepath.Abs(filepath.Join("..", ".."))
297385 if err != nil {
@@ -365,6 +453,34 @@ func TestTraceEnvironmentMatchesProcessMapping(t *testing.T) {
365453 }
366454 }
367455
456+func TestResolvePathOptionsMakesRelativeAndAbsoluteInputsIdentical(t *testing.T) {
457+ cwd, err := os.Getwd()
458+ if err != nil {
459+ t.Fatal(err)
460+ }
461+ absolute := options{
462+ vevBin: filepath.Join(cwd, "vev"),
463+ manifest: filepath.Join(cwd, "testdata", "perf", "manifest.json"),
464+ out: filepath.Join(cwd, "testdata", "perf", "results"),
465+ }
466+ relative := options{
467+ vevBin: "vev",
468+ manifest: "testdata/perf/manifest.json",
469+ out: "testdata/perf/results",
470+ }
471+ gotRelative, err := resolvePathOptions(relative)
472+ if err != nil {
473+ t.Fatal(err)
474+ }
475+ gotAbsolute, err := resolvePathOptions(absolute)
476+ if err != nil {
477+ t.Fatal(err)
478+ }
479+ if gotRelative != gotAbsolute {
480+ t.Fatalf("relative paths resolved to %+v, absolute paths to %+v", gotRelative, gotAbsolute)
481+ }
482+}
483+
368484 func TestHarnessFlagsRejectShortMeasurements(t *testing.T) {
369485 for _, args := range [][]string{
370486 {"--vev-bin", "vev", "--manifest", "m", "--out", "o", "--duration", "29s", "--repetitions", "10"},
@@ -292,6 +292,94 @@ func TestHarnessCanonicalLocalRolesAreIsolatedAcrossRepetitions(t *testing.T) {
292 }292 }
293 }293 }
294 294
295+func TestHarnessResolvesCanonicalRelativePathsBeforeRoleCWDChanges(t *testing.T) {
296+ if testing.Short() {
297+ t.Skip("public CLI relative-path lifecycle")
298+ }
299+ root, err := filepath.Abs(filepath.Join("..", ".."))
300+ if err != nil {
301+ t.Fatal(err)
302+ }
303+ // Keep the real Unix-socket path below its platform limit while testing the
304+ // exact documented relative output hierarchy.
305+ workspace, err := os.MkdirTemp("", "vev-rel-")
306+ if err != nil {
307+ t.Fatal(err)
308+ }
309+ t.Cleanup(func() { _ = os.RemoveAll(workspace) })
310+ bin := filepath.Join(workspace, "vev")
311+ build := exec.Command("/usr/local/go/bin/go", "build", "-o", bin, "./")
312+ build.Dir = root
313+ if output, err := build.CombinedOutput(); err != nil {
314+ t.Fatalf("build public CLI: %v\n%s", err, output)
315+ }
316+ manifestPath := filepath.Join(workspace, "testdata", "perf", "manifest.json")
317+ if err := os.MkdirAll(filepath.Dir(manifestPath), 0o700); err != nil {
318+ t.Fatal(err)
319+ }
320+ contents, err := os.ReadFile(filepath.Join(root, "testdata", "perf", "manifest.json"))
321+ if err != nil {
322+ t.Fatal(err)
323+ }
324+ if err := os.WriteFile(manifestPath, contents, 0o600); err != nil {
325+ t.Fatal(err)
326+ }
327+ originalWD, err := os.Getwd()
328+ if err != nil {
329+ t.Fatal(err)
330+ }
331+ if err := os.Chdir(workspace); err != nil {
332+ t.Fatal(err)
333+ }
334+ t.Cleanup(func() {
335+ if err := os.Chdir(originalWD); err != nil {
336+ t.Errorf("restore cwd: %v", err)
337+ }
338+ })
339+
340+ // These are the documented relative locations. cliLauncher changes every
341+ // role to its run directory, so a failure to resolve them before launch
342+ // would make VEV_PERF_TRACE point at a nonexistent nested directory.
343+ h := defaultHarness()
344+ h.clock = &fakeClock{}
345+ if err := run([]string{
346+ "--vev-bin", "./vev",
347+ "--manifest", "testdata/perf/manifest.json",
348+ "--out", "testdata/perf/results",
349+ "--scenario", "1x4-idle-local",
350+ "--warmup", "0s",
351+ "--duration", "30s",
352+ "--repetitions", "10",
353+ }, h); err != nil {
354+ t.Fatal(err)
355+ }
356+
357+ out := filepath.Join(workspace, "testdata", "perf", "results")
358+ for run := 1; run <= 10; run++ {
359+ runDir := filepath.Join(out, fmt.Sprintf("1x4-idle-local-run-%03d", run))
360+ manifestBytes, err := os.ReadFile(filepath.Join(runDir, "manifest.json"))
361+ if err != nil {
362+ t.Fatalf("run %d manifest: %v", run, err)
363+ }
364+ var persisted runManifest
365+ if err := json.Unmarshal(manifestBytes, &persisted); err != nil {
366+ t.Fatalf("run %d manifest JSON: %v", run, err)
367+ }
368+ if persisted.Run != run || len(persisted.Processes) != 2 {
369+ t.Fatalf("run %d manifest=%+v", run, persisted)
370+ }
371+ for _, process := range persisted.Processes {
372+ if !filepath.IsAbs(process.TracePath) || filepath.Dir(process.TracePath) != runDir {
373+ t.Fatalf("run %d %s trace path=%q, want absolute path in %q", run, process.Role, process.TracePath, runDir)
374+ }
375+ trace, err := os.ReadFile(process.TracePath)
376+ if err != nil || len(bytes.TrimSpace(trace)) == 0 {
377+ t.Fatalf("run %d %s trace=%q err=%v", run, process.Role, process.TracePath, err)
378+ }
379+ }
380+ }
381+}
382+
295 func TestHarnessScenarioSelectionKeepsCanonicalManifestValidation(t *testing.T) {383 func TestHarnessScenarioSelectionKeepsCanonicalManifestValidation(t *testing.T) {
296 root, err := filepath.Abs(filepath.Join("..", ".."))384 root, err := filepath.Abs(filepath.Join("..", ".."))
297 if err != nil {385 if err != nil {
@@ -365,6 +453,34 @@ func TestTraceEnvironmentMatchesProcessMapping(t *testing.T) {
365 }453 }
366 }454 }
367 455
456+func TestResolvePathOptionsMakesRelativeAndAbsoluteInputsIdentical(t *testing.T) {
457+ cwd, err := os.Getwd()
458+ if err != nil {
459+ t.Fatal(err)
460+ }
461+ absolute := options{
462+ vevBin: filepath.Join(cwd, "vev"),
463+ manifest: filepath.Join(cwd, "testdata", "perf", "manifest.json"),
464+ out: filepath.Join(cwd, "testdata", "perf", "results"),
465+ }
466+ relative := options{
467+ vevBin: "vev",
468+ manifest: "testdata/perf/manifest.json",
469+ out: "testdata/perf/results",
470+ }
471+ gotRelative, err := resolvePathOptions(relative)
472+ if err != nil {
473+ t.Fatal(err)
474+ }
475+ gotAbsolute, err := resolvePathOptions(absolute)
476+ if err != nil {
477+ t.Fatal(err)
478+ }
479+ if gotRelative != gotAbsolute {
480+ t.Fatalf("relative paths resolved to %+v, absolute paths to %+v", gotRelative, gotAbsolute)
481+ }
482+}
483+
368 func TestHarnessFlagsRejectShortMeasurements(t *testing.T) {484 func TestHarnessFlagsRejectShortMeasurements(t *testing.T) {
369 for _, args := range [][]string{485 for _, args := range [][]string{
370 {"--vev-bin", "vev", "--manifest", "m", "--out", "o", "--duration", "29s", "--repetitions", "10"},486 {"--vev-bin", "vev", "--manifest", "m", "--out", "o", "--duration", "29s", "--repetitions", "10"},

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