Files
sausage/host/tests/p4.rs
T

278 lines
9.1 KiB
Rust

//! P4 exit: the real console agent builds Rust and Node projects, then inspects undo history.
use anyhow::{Context, ensure};
use sausage::world::World;
use sausage_vfs::journal::Entry;
use std::{
fs,
io::Write,
path::{Path, PathBuf},
process::{Child, Command, Stdio},
time::{Duration, Instant},
};
fn checked(command: &mut Command) -> anyhow::Result<()> {
ensure!(command.status()?.success(), "command failed: {command:?}");
Ok(())
}
fn prepare(dir: &Path) -> anyhow::Result<PathBuf> {
let source = Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
let userland = source.join("userland");
let target = PathBuf::from(std::env::var("CARGO_TARGET_DIR")?);
let bundle = Path::new(env!("CARGO_TARGET_TMPDIR")).join("p4-bundle");
let revision = Command::new("git")
.current_dir(&source)
.args(["rev-parse", "HEAD"])
.output()?;
ensure!(revision.status.success(), "fixture revision");
if fs::read(bundle.join("revision")).ok().as_deref() != Some(revision.stdout.as_slice()) {
if !userland.join("cwd-init/init.o").exists() {
checked(Command::new("nu").arg(userland.join("cwd-init/build.nu")))?;
}
checked(Command::new("cargo").current_dir(&userland).args([
"build",
"--release",
"--locked",
"-p",
"git",
"-p",
"world-cargo",
"-p",
"box-cli",
"-p",
"undo",
"-p",
"sausage-coreutils",
]))?;
checked(Command::new("nu").arg(userland.join("brush/build.nu")))?;
fs::create_dir_all(bundle.join("bin"))?;
fs::create_dir_all(bundle.join("src"))?;
for (binary, name) in [
("brush", "sh"),
("brush", "bash"),
("git", "git"),
("cargo", "cargo"),
("box", "box"),
("undo", "undo"),
("cat", "cat"),
("mkdir", "mkdir"),
] {
fs::copy(
target.join(format!("wasm32-wasip2/release/{binary}.wasm")),
bundle.join(format!("bin/{name}.wasm")),
)?;
}
fs::write(bundle.join("revision"), revision.stdout)?;
}
World::open_with_userland(dir, Some(&bundle))?;
let repo = dir.join("world/src/sausage");
fs::create_dir_all(repo.join("extensions/p4"))?;
fs::write(
repo.join("extensions/p4/server.ts"),
include_str!("../js/p4-test-bundle.js"),
)?;
for args in [
vec!["init", "-b", "main"],
vec!["add", "."],
vec!["commit", "--no-gpg-sign", "-m", "P4 fixture"],
] {
checked(
Command::new("git")
.current_dir(&repo)
.args(args)
.env("GIT_AUTHOR_NAME", "P4 test")
.env("GIT_AUTHOR_EMAIL", "p4@example.com")
.env("GIT_COMMITTER_NAME", "P4 test")
.env("GIT_COMMITTER_EMAIL", "p4@example.com"),
)?;
}
Ok(bundle)
}
struct Running(Child);
impl Drop for Running {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
fn round(dir: &Path, bundle: &Path, number: usize) -> anyhow::Result<()> {
let stdout = dir.join(format!("round-{number}.stdout"));
let stderr = dir.join(format!("round-{number}.stderr"));
let mut child = Running(
Command::new(env!("CARGO_BIN_EXE_sausage"))
.arg("run")
.arg(dir)
.arg("--userland")
.arg(bundle)
.args(["--api-listen", "127.0.0.1:0"])
.env("PI_PROXY_BASE_URL", "https://proxy.invalid")
.env("PI_PROXY_API_KEY", "faux-token")
.env_remove("CREDENTIALS_DIRECTORY")
.stdin(Stdio::piped())
.stdout(fs::File::create(&stdout)?)
.stderr(fs::File::create(&stderr)?)
.spawn()?,
);
writeln!(
child.0.stdin.take().context("console stdin")?,
"P4 start {number}"
)?;
let deadline = Instant::now() + Duration::from_secs(600);
loop {
if fs::read_to_string(&stdout)?.contains(&format!("P4_COMPLETE_{number}")) {
break;
}
if let Some(status) = child.0.try_wait()? {
anyhow::bail!("console exited: {status}");
}
ensure!(
Instant::now() < deadline,
"P4 round {number} exceeded its deadline"
);
std::thread::yield_now();
}
checked(Command::new("kill").args(["-TERM", &child.0.id().to_string()]))?;
let deadline = Instant::now() + Duration::from_secs(20);
loop {
if let Some(status) = child.0.try_wait()? {
ensure!(status.success(), "console shutdown: {status}");
break;
}
ensure!(
Instant::now() < deadline,
"console shutdown exceeded its deadline"
);
std::thread::yield_now();
}
Ok(())
}
fn boxes_removed(dir: &Path, bundle: &Path) -> anyhow::Result<()> {
let path = dir.join("world/tmp/p4-box-id");
if !path.exists() {
return Ok(());
}
let id = fs::read_to_string(path)?;
let id = id.trim();
ensure!(
id.starts_with("tc-") && id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-'),
"invalid box ID"
);
// The agent's EXIT trap is primary cleanup. Recover its box even if the console fails.
let output = Command::new(env!("CARGO_BIN_EXE_sausage"))
.arg("sh")
.arg(dir)
.arg("--userland")
.arg(bundle)
.arg("-c")
.arg("box ls")
.env_remove("CREDENTIALS_DIRECTORY")
.output()?;
ensure!(
output.status.success(),
"box ls: {}",
String::from_utf8_lossy(&output.stderr)
);
if String::from_utf8_lossy(&output.stdout).contains(id) {
checked(
Command::new(env!("CARGO_BIN_EXE_sausage"))
.arg("sh")
.arg(dir)
.arg("--userland")
.arg(bundle)
.arg("-c")
.arg(format!("box rm {id}"))
.env_remove("CREDENTIALS_DIRECTORY"),
)?;
anyhow::bail!("Agent left {id} active. Test cleanup removed it");
}
println!("P4 box absent from service inventory: {id}");
Ok(())
}
fn cache_hits(dir: &Path) -> anyhow::Result<u64> {
let workspace = fs::read_dir(dir.join("host/builds"))?
.next()
.context("builder workspace")??
.path();
let output = Command::new("kache")
.arg("stats")
.env("KACHE_CACHE_DIR", workspace.join("kache"))
.output()?;
ensure!(
output.status.success(),
"kache: {}",
String::from_utf8_lossy(&output.stderr)
);
let text = String::from_utf8(output.stdout)?;
println!("P4 PRIVATE CACHE:\n{text}");
let words: Vec<_> = text.split_whitespace().collect();
words
.windows(2)
.find_map(|pair| {
if pair[1] == "of" {
pair[0].replace(',', "").parse().ok()
} else {
None
}
})
.context("cache hit count")
}
#[test]
fn agent_builds_rust_and_node_twice() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let bundle = prepare(dir.path())?;
let mut first_hits = 0;
for number in 1..=2 {
let outcome = round(dir.path(), &bundle, number);
let cleanup = boxes_removed(dir.path(), &bundle);
if outcome.is_err() {
for stream in ["stdout", "stderr"] {
eprintln!(
"P4 round {number} {stream}:\n{}",
fs::read_to_string(dir.path().join(format!("round-{number}.{stream}")))?
);
}
}
outcome?;
cleanup?;
let world = dir.path().join("world");
ensure!(
world
.join("src/demo-cli/target/wasm32-wasip2/release/demo-cli.wasm")
.is_file(),
"missing world artifact"
);
ensure!(
fs::read_to_string(world.join("src/demo-cli/Cargo.toml"))?.contains("serde_json"),
"cargo add did not reach world"
);
ensure!(
fs::read_to_string(world.join("src/demo-node/result.txt"))? == "P4 npm test passed\n",
"Node result missing"
);
let entries: Vec<Entry> =
serde_json::from_slice(&fs::read(world.join("tmp/p4-undo.json"))?)?;
for (phase, path) in [
(0, "/src/demo-cli/Cargo.toml"),
(1, "/src/demo-cli/src/main.rs"),
] {
let tag = format!("p4-{number}-{phase}");
ensure!(
entries.iter().any(|entry| entry.tag == tag
&& entry.program == "bash"
&& entry.changed_paths.iter().any(|p| p == path)),
"undo lost {tag}: {path}"
);
}
let hits = cache_hits(dir.path())?;
if number == 1 {
first_hits = hits;
} else {
ensure!(
hits > first_hits,
"second run missed kache: {first_hits} -> {hits}"
);
println!("P4 CACHE SECOND RUN: {first_hits} -> {hits} hits");
}
}
Ok(())
}