fix(obs): resolve harness output paths
- commit
- be0d9080ee17a72aab76bc80ebe7ff8e3d50e232
- author
- brice · · 2026-07-13 01:54 UTC
- parent
- 51273ff
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 { | |||
| 197 | 197 | netemFactory udpNetemFactory | |
| 198 | 198 | mu sync.Mutex | |
| 199 | 199 | peers map[string]peerRoute | |
| 200 | + | runtimes map[string]string | |
| 200 | 201 | } | |
| 201 | 202 | ||
| 202 | 203 | type peerRoute struct { | |
| @@ -206,7 +207,10 @@ type peerRoute struct { | |||
| 206 | 207 | 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 | 215 | func (p preparedPeer) Warmup([]byte) error { return nil } | |
| 212 | 216 | func (p preparedPeer) Measure([]byte, func() error, func() error) error { return nil } | |
| @@ -230,6 +234,11 @@ func (p preparedPeer) Close() error { | |||
| 230 | 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 | 242 | return errors.Join(errs...) | |
| 234 | 243 | } | |
| 235 | 244 | ||
| @@ -240,19 +249,20 @@ type terminalOutput interface { | |||
| 240 | 249 | } | |
| 241 | 250 | ||
| 242 | 251 | 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 | |
| 256 | 266 | } | |
| 257 | 267 | ||
| 258 | 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 | 280 | cmd := exec.Command(l.bin, role.Args...) | |
| 271 | 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 | 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 | 294 | "XDG_STATE_HOME="+filepath.Join(runDir, "state"), "TERM=xterm-256color") | |
| 275 | 295 | if m.Role == "client" { | |
| 276 | 296 | l.mu.Lock() | |
| @@ -284,13 +304,13 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces | |||
| 284 | 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 | 307 | if err := safedir.EnsurePrivate(filepath.Join(runDir, "state")); err != nil { | |
| 291 | 308 | 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 | 314 | if m.Role == "client" { | |
| 295 | 315 | master, slave, err := openPTY() | |
| 296 | 316 | if err != nil { | |
| @@ -309,6 +329,9 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces | |||
| 309 | 329 | _ = output.Close() | |
| 310 | 330 | _ = master.Close() | |
| 311 | 331 | _ = slave.Close() | |
| 332 | + | if p.cleanupRuntime != nil { | |
| 333 | + | _ = p.cleanupRuntime() | |
| 334 | + | } | |
| 312 | 335 | return nil, err | |
| 313 | 336 | } | |
| 314 | 337 | _ = slave.Close() | |
| @@ -325,6 +348,9 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces | |||
| 325 | 348 | cmd.Stdin, cmd.Stdout, cmd.Stderr = devNull, devNull, devNull | |
| 326 | 349 | if err := cmd.Start(); err != nil { | |
| 327 | 350 | _ = devNull.Close() | |
| 351 | + | if p.cleanupRuntime != nil { | |
| 352 | + | _ = p.cleanupRuntime() | |
| 353 | + | } | |
| 328 | 354 | return nil, err | |
| 329 | 355 | } | |
| 330 | 356 | _ = devNull.Close() | |
| @@ -345,6 +371,41 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces | |||
| 345 | 371 | 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 | 409 | // preparePeer installs a per-run ssh command seam. The ordinary public client | |
| 349 | 410 | // still executes its documented remote attach command; its ssh child invokes | |
| 350 | 411 | // 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 | 413 | // separately launched but unused process. | |
| 353 | 414 | func (l *cliLauncher) preparePeer(m processMapping, role roleCommand) (launchedProcess, error) { | |
| 354 | 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 | 427 | if err := safedir.EnsurePrivate(runDir); err != nil { | |
| 356 | 428 | 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 | 431 | return nil, err | |
| 360 | 432 | } | |
| 361 | 433 | if err := safedir.EnsurePrivate(filepath.Join(runDir, "state")); err != nil { | |
| @@ -416,7 +488,7 @@ case "$*" in | |||
| 416 | 488 | ;; | |
| 417 | 489 | *) echo 'vev harness ssh seam rejected non-vev command' >&2; exit 64 ;; | |
| 418 | 490 | 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 | 492 | if err := os.WriteFile(shim, []byte(body), 0o700); err != nil { | |
| 421 | 493 | if route.netem != nil { | |
| 422 | 494 | _ = route.netem.Close() | |
| @@ -436,7 +508,11 @@ esac | |||
| 436 | 508 | } | |
| 437 | 509 | l.peers[runDir] = route | |
| 438 | 510 | 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 | |
| 440 | 516 | } | |
| 441 | 517 | ||
| 442 | 518 | func remoteMode(t transport) (string, error) { | |
| @@ -744,6 +820,11 @@ func (p *cliProcess) Close() error { | |||
| 744 | 820 | 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 | 829 | return result | |
| 749 | 830 | } | |
| @@ -873,6 +954,10 @@ func run(args []string, h *harness) error { | |||
| 873 | 954 | if err != nil { | |
| 874 | 955 | return err | |
| 875 | 956 | } | |
| 957 | + | opt, err = resolvePathOptions(opt) | |
| 958 | + | if err != nil { | |
| 959 | + | return err | |
| 960 | + | } | |
| 876 | 961 | m, err := readManifest(opt.manifest) | |
| 877 | 962 | if err != nil { | |
| 878 | 963 | return err | |
| @@ -972,6 +1057,27 @@ func parseOptions(args []string) (options, error) { | |||
| 972 | 1057 | } | |
| 973 | 1058 | 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 | 1081 | func readManifest(path string) (manifest, error) { | |
| 976 | 1082 | f, e := os.Open(path) | |
| 977 | 1083 | if e != nil { | |
| @@ -197,6 +197,7 @@ type cliLauncher struct { | |||||
| 197 | netemFactory udpNetemFactory | 197 | netemFactory udpNetemFactory | ||
| 198 | mu sync.Mutex | 198 | mu sync.Mutex | ||
| 199 | peers map[string]peerRoute | 199 | 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 udpNetem | 207 | 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.Cmd | 252 | + | cmd *exec.Cmd |
| 244 | - | pty io.ReadWriteCloser | 253 | + | pty io.ReadWriteCloser |
| 245 | - | output terminalOutput | 254 | + | output terminalOutput |
| 246 | - | chunks chan []byte | 255 | + | chunks chan []byte |
| 247 | - | done chan struct{} | 256 | + | done chan struct{} |
| 248 | - | closed sync.Once | 257 | + | closed sync.Once |
| 249 | - | waitErr chan error | 258 | + | waitErr chan error |
| 250 | - | waitTimeout func() <-chan time.Time | 259 | + | waitTimeout func() <-chan time.Time |
| 251 | - | forceCleanup func() | 260 | + | forceCleanup func() |
| 252 | - | forceKill func() | 261 | + | forceKill func() |
| 253 | - | shutdown func() error | 262 | + | shutdown func() error |
| 254 | - | terminalReady bool | 263 | + | cleanupRuntime func() error |
| 255 | - | readyPath string | 264 | + | 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, err | 308 | 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, err | 335 | 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, devNull | 348 | 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, err | 354 | 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, nil | 371 | 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 client | 409 | // preparePeer installs a per-run ssh command seam. The ordinary public client | ||
| 349 | // still executes its documented remote attach command; its ssh child invokes | 410 | // still executes its documented remote attach command; its ssh child invokes | ||
| 350 | // only the parsed public _stdio or _udp-proxy command. This makes the peer | 411 | // 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, err | 428 | 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, err | 431 | 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 | esac | 490 | 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] = route | 509 | l.peers[runDir] = route | ||
| 438 | l.mu.Unlock() | 510 | 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 | |||
| 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 = err | 820 | 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 result | 829 | 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 err | 955 | 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 err | 963 | return err | ||
| @@ -972,6 +1057,27 @@ func parseOptions(args []string) (options, error) { | |||||
| 972 | } | 1057 | } | ||
| 973 | return o, nil | 1058 | 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) { | |||
| 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 | 383 | func TestHarnessScenarioSelectionKeepsCanonicalManifestValidation(t *testing.T) { | |
| 296 | 384 | root, err := filepath.Abs(filepath.Join("..", "..")) | |
| 297 | 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 | 484 | func TestHarnessFlagsRejectShortMeasurements(t *testing.T) { | |
| 369 | 485 | for _, args := range [][]string{ | |
| 370 | 486 | {"--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"}, | ||