ci / rust (push) Successful in 2m8s
New `crates/maestro-e2e` exercises the full headless stack — registry, two daemons, real HTTP+WS — as actual subprocesses. Harness pieces: - `Registry::spawn` and `Daemon::spawn` launch the real built binaries with `env_clear()` plus an isolated `$HOME`, `XDG_CONFIG_HOME`, `XDG_STATE_HOME`, and (for daemons) `TMUX_TMPDIR`. Drop guards kill the child and clean the temp dirs. - `Daemon::stop` + `Daemon::restart` swap the process without losing the state dirs, so the recovery test exercises a real tmux re-adoption. - A `build::workspace_target_bin` helper re-invokes cargo from the test process to build `maestrod` / `maestro-registry` on demand (cargo only ships libs as path-dep of e2e by default). - `TestClient` wraps reqwest + tokio-tungstenite for the public HTTP+WS surface — list/spawn/kill/connect-info/attach + events WS. Scenarios covered (`tests/e2e.rs`): - list sessions from two daemons through one registry - multi-attach: two WS clients see the same PTY output - worktree with `cleanup_on_kill: true` is removed after kill - daemon restart: catalog drops then repopulates from `recover()` - concurrent spawns all get distinct ids All five pass in ~0.7s in parallel; cleanup confirmed (no leftover processes or temp dirs).
73 lines
2.7 KiB
Rust
73 lines
2.7 KiB
Rust
//! Locate / build the maestro binaries needed by the harness.
|
|
//!
|
|
//! `cargo test -p maestro-e2e` does not, by default, rebuild bins from other
|
|
//! workspace crates. So we re-invoke cargo from the first test that needs a
|
|
//! binary and cache the resolved paths. The re-invocation acquires its own
|
|
//! workspace lock — by the time tests start, the outer cargo has already
|
|
//! released it.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::OnceLock;
|
|
|
|
/// Absolute path of the workspace root (the dir containing the top-level
|
|
/// `Cargo.toml` whose body has `[workspace]`).
|
|
pub fn workspace_root() -> &'static Path {
|
|
static ROOT: OnceLock<PathBuf> = OnceLock::new();
|
|
ROOT.get_or_init(|| {
|
|
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
let mut cur: &Path = manifest.as_path();
|
|
loop {
|
|
let candidate = cur.join("Cargo.toml");
|
|
if candidate.exists() {
|
|
if let Ok(body) = std::fs::read_to_string(&candidate) {
|
|
if body.contains("[workspace]") {
|
|
return cur.to_path_buf();
|
|
}
|
|
}
|
|
}
|
|
match cur.parent() {
|
|
Some(p) => cur = p,
|
|
None => panic!(
|
|
"could not find workspace root starting from {}",
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR")).display()
|
|
),
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Resolve `target/<profile>/<name>` under the workspace root, after running
|
|
/// `cargo build --bin <name> -p <package>` to make sure it exists.
|
|
pub fn workspace_target_bin(package: &str, bin: &str) -> PathBuf {
|
|
static BUILDS: OnceLock<std::sync::Mutex<Vec<String>>> = OnceLock::new();
|
|
let builds = BUILDS.get_or_init(|| std::sync::Mutex::new(Vec::new()));
|
|
|
|
let already = {
|
|
let lock = builds.lock().unwrap();
|
|
lock.iter().any(|b| b == bin)
|
|
};
|
|
let release = !cfg!(debug_assertions);
|
|
if !already {
|
|
let mut cmd = std::process::Command::new(env!("CARGO"));
|
|
cmd.args(["build", "--quiet", "-p", package, "--bin", bin]);
|
|
if release {
|
|
cmd.arg("--release");
|
|
}
|
|
let status = cmd
|
|
.current_dir(workspace_root())
|
|
.status()
|
|
.unwrap_or_else(|e| panic!("failed to invoke cargo build for {bin}: {e}"));
|
|
assert!(status.success(), "cargo build for `{bin}` failed");
|
|
builds.lock().unwrap().push(bin.to_string());
|
|
}
|
|
|
|
let profile = if release { "release" } else { "debug" };
|
|
let path = workspace_root().join("target").join(profile).join(bin);
|
|
assert!(
|
|
path.exists(),
|
|
"expected built binary at {} after `cargo build`",
|
|
path.display()
|
|
);
|
|
path
|
|
}
|