host: review fixes for cargo.build, the test runner, the console, and main; tests for every PROTOCOL.md rule
cargo.build and the runner (review T22R): builds use the network again; core.unlock and core.notify in order under the runner; quiet test output; manifest preparation keeps [workspace], target tables, renamed and optional entries; cargo.rpc.discover; -32004 with data.output; retention of 10 builds; light, empty test worlds with an in-memory journal; only CARGO_/RUST_/SAUSAGE_ variables; stdin is null; the runner through --config. A compile cache that cannot be created is not fatal. Activation replaces plugin.wasm without a gap. Console and main: init ships the workspace Cargo.lock and creates the world atomically; each console command runs in its own task, so ps, stop, and kill work during a slow call; SAUSAGE_HEADLESS; the terminal mode is restored; signals during boot; chat text is not taken as a command. Tests: 20 more tests for PROTOCOL.md sections 2 to 10; the cargo tests choose the libtest binary from cargo's JSON output and time out.
This commit is contained in:
+64
-25
@@ -3,14 +3,12 @@ use anyhow::{Result, bail};
|
||||
use sausage::{
|
||||
Host,
|
||||
runtime::{Exit, InstanceConfig, Runtime},
|
||||
world::World,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use wasmtime::component::Component;
|
||||
|
||||
/// A temporary world remains alive through the end of its WASI store.
|
||||
@@ -32,19 +30,36 @@ async fn instance(
|
||||
package: PathBuf,
|
||||
) -> Result<Run> {
|
||||
let path = std::env::temp_dir().join(format!("sausage-test-{}", uuid::Uuid::now_v7()));
|
||||
World::create(&path)?;
|
||||
let world = TestWorld(path);
|
||||
let host = Host::start(&world.0).await?;
|
||||
let host = Host::test_world(&world.0, runtime.clone()).await?;
|
||||
let tracker = sausage_vfs::Process::default();
|
||||
let mut filesystem = host.filesystem().await?;
|
||||
// Virtual ancestors keep directory entries of the absolute package mount traversable.
|
||||
for ancestor in package
|
||||
.ancestors()
|
||||
.skip(1)
|
||||
.filter(|path| *path != Path::new("/"))
|
||||
{
|
||||
filesystem.mount(
|
||||
ancestor.to_str().unwrap(),
|
||||
Arc::new(sausage_vfs::Memory::default()),
|
||||
false,
|
||||
);
|
||||
}
|
||||
filesystem.mount(
|
||||
package.to_str().unwrap(),
|
||||
Arc::new(sausage_vfs::Host::new(&package, tracker.clone())?),
|
||||
false,
|
||||
);
|
||||
let mut env: Vec<_> = std::env::vars().filter(|(key, _)| key != "PWD").collect();
|
||||
let mut env: Vec<_> = std::env::vars_os()
|
||||
.filter_map(|(key, value)| Some((key.into_string().ok()?, value.into_string().ok()?)))
|
||||
.filter(|(key, _)| {
|
||||
["CARGO_", "RUST_", "SAUSAGE_"]
|
||||
.iter()
|
||||
.any(|prefix| key.starts_with(prefix))
|
||||
})
|
||||
.collect();
|
||||
env.extend([
|
||||
("PWD".into(), package.display().to_string()),
|
||||
("SAUSAGE_PID".into(), "test".into()),
|
||||
("SAUSAGE_PLUGIN".into(), "test".into()),
|
||||
("SAUSAGE_PARENT".into(), "core".into()),
|
||||
@@ -61,23 +76,38 @@ async fn instance(
|
||||
&args,
|
||||
)
|
||||
.await?;
|
||||
let mut stdin = guest.stdin.take().unwrap();
|
||||
let mut stdout = std::mem::replace(&mut guest.stdout, tokio::sync::mpsc::channel(1).1);
|
||||
let mut pending = std::collections::VecDeque::new();
|
||||
let mut stderr = std::mem::replace(&mut guest.stderr, tokio::sync::mpsc::channel(1).1);
|
||||
let mut exit = None;
|
||||
let (errors, mut stderr_lines) = tokio::sync::mpsc::unbounded_channel();
|
||||
let stderr_task = tokio::spawn(async move {
|
||||
while let Some(line) = stderr.recv().await {
|
||||
let _ = errors.send(line);
|
||||
}
|
||||
});
|
||||
let mut output = String::new();
|
||||
let wait = guest.wait();
|
||||
tokio::pin!(wait);
|
||||
let mut stdout_open = true;
|
||||
let mut stderr_open = true;
|
||||
let mut handlers = tokio::task::JoinSet::new();
|
||||
while exit.is_none() || stdout_open || stderr_open {
|
||||
while stdout_open || stderr_open || !pending.is_empty() {
|
||||
tokio::select! {
|
||||
result=&mut wait, if exit.is_none()=>exit=Some(result),
|
||||
line=stdout.recv(), if stdout_open=>match line {
|
||||
line=async { if let Some(line) = pending.pop_front() { Some(line) } else { guest.stdout.recv().await } }, if stdout_open || !pending.is_empty()=>match line {
|
||||
Some(line)=> {
|
||||
// Libtest can prefix its status line before SDK output. The SDK starts JSON on a fresh line.
|
||||
if let Ok(request)=serde_json::from_str::<Value>(&line) && request["jsonrpc"]=="2.0" && request["method"].is_string() {
|
||||
let method = request["method"].as_str().unwrap();
|
||||
if !matches!(method, "core.lock" | "core.log") {
|
||||
let params = request.get("params").cloned().unwrap_or(json!({}));
|
||||
let result = match method {
|
||||
"core.unlock" => host.call(method, params).await,
|
||||
"core.notify" => Ok(Value::Null),
|
||||
_ => Err(json!({"code":-32601,"message":"a test has no other plugins"})),
|
||||
};
|
||||
if let Some(id) = request.get("id") {
|
||||
let response = match result { Ok(result)=>json!({"jsonrpc":"2.0","id":id,"result":result}), Err(error)=>json!({"jsonrpc":"2.0","id":id,"error":error}) };
|
||||
if guest.send_line(&response.to_string(), |line| pending.push_back(line)).await.is_err() { guest.stdin.take(); }
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let host = host.clone();
|
||||
handlers.spawn(async move {
|
||||
let method=request["method"].as_str().unwrap();
|
||||
@@ -89,25 +119,22 @@ async fn instance(
|
||||
};
|
||||
request.get("id").map(|id|match result {Ok(result)=>json!({"jsonrpc":"2.0","id":id,"result":result}),Err(error)=>json!({"jsonrpc":"2.0","id":id,"error":error})})
|
||||
});
|
||||
} else {output.push_str(&line);output.push('\n');}
|
||||
} else {output.push_str(line.trim_end_matches(['\r', '\n']));output.push('\n');}
|
||||
},None=>stdout_open=false,
|
||||
},
|
||||
line=stderr.recv(), if stderr_open=>match line {Some(line)=>{output.push_str(&line);output.push('\n');},None=>stderr_open=false},
|
||||
line=stderr_lines.recv(), if stderr_open=>match line {Some(line)=>{output.push_str(&line);output.push('\n');},None=>stderr_open=false},
|
||||
response=handlers.join_next(), if !handlers.is_empty()=> {
|
||||
if let Some(response) = response.transpose()?.flatten() {
|
||||
stdin.write_all(format!("{response}\n").as_bytes()).await?;
|
||||
stdin.flush().await?;
|
||||
}
|
||||
if let Some(response) = response.transpose()?.flatten()
|
||||
&& guest.send_line(&response.to_string(), |line| pending.push_back(line)).await.is_err() { guest.stdin.take(); }
|
||||
},
|
||||
}
|
||||
}
|
||||
handlers.abort_all();
|
||||
let exit = guest.wait().await;
|
||||
let _ = stderr_task.await;
|
||||
tracker.idle().await;
|
||||
host.stop().await;
|
||||
Ok(Run {
|
||||
exit: exit.unwrap(),
|
||||
output,
|
||||
})
|
||||
Ok(Run { exit, output })
|
||||
}
|
||||
/// Extracts libtest names from its stable list format.
|
||||
fn names(output: &str) -> Vec<String> {
|
||||
@@ -220,7 +247,19 @@ pub async fn run(wasm: &Path, args: Vec<String>) -> Result<bool> {
|
||||
for line in run
|
||||
.output
|
||||
.lines()
|
||||
.filter(|line| !line.starts_with("test result:"))
|
||||
.filter(|line| {
|
||||
!line.trim().is_empty()
|
||||
&& !line.starts_with("test result:")
|
||||
&& !matches!(*line, "running 1 test" | "running 0 tests")
|
||||
&& !matches!(*line, "ok" | "FAILED")
|
||||
})
|
||||
.map(|line| {
|
||||
line.strip_prefix(&format!("test {name} ... "))
|
||||
.unwrap_or(line)
|
||||
})
|
||||
.filter(|line| {
|
||||
!matches!(*line, "ok" | "FAILED" | "ignored") && !line.is_empty()
|
||||
})
|
||||
{
|
||||
// Indentation keeps ordinary test output separate from our libtest status and totals.
|
||||
println!(" {line}");
|
||||
|
||||
Reference in New Issue
Block a user