Files
sausage/sdk/tests/wasi.rs
T
surma 8d3c163992 sausage-plugin part 2: import! with typed clients (sausage_interface::client_rs), TCP endpoints with rpc.endpoint, examples
Part 1 fixes: complete stdout writes, buffered stdin, Rust type names
in schemas, enum variant docs, -32603 for serde errors in handlers,
crate docs and modules. End-to-end check of a generated consumer through
a fake core.
2026-10-23 00:35:45 +01:00

246 lines
9.5 KiB
Rust

//! End-to-end protocol checks against a WASI plugin under Wasmtime.
use sausage_plugin::serde_json::{Value, json};
use std::{
io::{BufRead, BufReader, Write},
path::{Path, PathBuf},
process::{Command, Stdio},
};
struct Process {
child: std::process::Child,
input: Option<std::process::ChildStdin>,
output: BufReader<std::process::ChildStdout>,
}
impl Process {
fn send(&mut self, v: Value) {
writeln!(self.input.as_mut().unwrap(), "{v}").unwrap();
}
fn read(&mut self) -> Value {
loop {
let mut s = String::new();
assert_ne!(self.output.read_line(&mut s).unwrap(), 0, "unexpected EOF");
if let Ok(v) = sausage_plugin::serde_json::from_str::<Value>(&s)
&& v["jsonrpc"] == "2.0"
{
return v;
}
}
}
fn request(&mut self, method: &str, params: Value) {
self.send(json!({"jsonrpc":"2.0","id":"probe-1","method":method,"params":params}));
}
fn answer(&mut self, r: &Value, result: Value) {
self.send(json!({"jsonrpc":"2.0","id":r["id"],"result":result}));
}
}
fn start(wasm: &Path, dir: &Path) -> Process {
let mut child = Command::new("wasmtime")
.args([
"run",
"-S",
"inherit-network=y",
"--env",
"SAUSAGE_PID=probe-00000000-0000-7000-8000-000000000000",
"--env",
"SAUSAGE_PLUGIN=probe",
"--dir",
])
.arg(format!("{}::/", dir.display()))
.arg(wasm)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let input = child.stdin.take();
let output = BufReader::new(child.stdout.take().unwrap());
Process {
child,
input,
output,
}
}
#[test]
fn protocol() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let target = root.join("../target/sdk/probe");
assert!(
Command::new("cargo")
.env("CARGO_TARGET_DIR", &target)
.args(["build", "--offline", "--manifest-path"])
.arg(root.join("tests/plugins/probe/Cargo.toml"))
.args(["--target", "wasm32-wasip2"])
.status()
.unwrap()
.success()
);
let wasm = target.join("wasm32-wasip2/debug/sdk-probe.wasm");
let dir = std::env::temp_dir().join(format!("sausage-sdk-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let mut p = start(&wasm, &dir);
p.request("rpc.discover", json!({}));
let d = p.read();
assert_eq!(d["result"]["x-sausage-grace"], 12);
assert_eq!(
d["result"]["methods"]
.as_array()
.unwrap()
.iter()
.find(|m| m["name"] == "add")
.unwrap()["description"],
"Add two numbers.\n\nThe result is their sum."
);
assert!(p.child.wait().unwrap().success());
let mut p = start(&wasm, &dir);
p.request("add", json!({"a":2,"b":4}));
assert_eq!(p.read()["result"], 6);
assert!(p.child.wait().unwrap().success());
let mut p = start(&wasm, &dir);
p.request("echo", json!({"text":"x".repeat(70_000)}));
assert_eq!(p.read()["result"], "x".repeat(70_000));
assert!(p.child.wait().unwrap().success());
let mut p = start(&wasm, &dir);
p.request("echo", json!({"text":5}));
assert_eq!(p.read()["error"]["code"], -32602);
assert!(p.child.wait().unwrap().success());
let mut p = start(&wasm, &dir);
p.request("serde_error", json!({}));
assert_eq!(p.read()["error"]["code"], -32603);
assert!(p.child.wait().unwrap().success());
let mut p = start(&wasm, &dir);
p.request("fail", json!({}));
assert_eq!(p.read()["error"]["code"], -32010);
assert!(p.child.wait().unwrap().success());
let mut p = start(&wasm, &dir);
p.request("exercise", json!({}));
let a = p.read();
let b = p.read();
assert_eq!(a["method"], "other.first");
assert_eq!(b["method"], "core.call");
assert_eq!(b["params"]["detach"], true);
p.send(json!({"jsonrpc":"2.0","id":9876,"result":{}}));
p.send(json!({"jsonrpc":"2.0","method":"core.notify","params":{"call":a["id"],"name":"tick","params":{}}}));
p.answer(&b, json!({}));
p.answer(&a, json!({}));
assert_eq!(p.read()["method"], "core.notify");
let grant = p.read();
assert_eq!(grant["method"], "core.lock");
p.answer(&grant, json!({"held":true}));
assert_eq!(p.read()["method"], "core.unlock");
let attempt = p.read();
assert_eq!(attempt["params"]["wait"], false);
p.answer(&attempt, json!({"held":false}));
assert_eq!(p.read()["result"], "done");
assert!(p.child.wait().unwrap().success());
let mut err = String::new();
std::io::Read::read_to_string(p.child.stderr.as_mut().unwrap(), &mut err).unwrap();
assert!(err.contains("unknown ID 9876"));
assert!(err.contains("notification: tick"));
let mut p = start(&wasm, &dir);
p.request("rpc.retired", json!({"previous":"hash"}));
assert_eq!(p.read()["result"], "hash");
assert!(p.child.wait().unwrap().success());
let mut p = start(&wasm, &dir);
p.request("late", json!({}));
let grant = p.read();
let barrier = p.read();
p.answer(&grant, json!({"held":true}));
assert_eq!(p.read()["method"], "core.unlock");
p.answer(&barrier, json!({}));
let kept = p.read();
p.answer(&kept, json!({"held":true}));
assert_eq!(p.read()["result"], "late done");
assert!(p.child.wait().unwrap().success());
let mut p = start(&wasm, &dir);
p.request("wait", json!({}));
assert_eq!(p.read()["method"], "other.wait");
drop(p.input.take());
assert_eq!(p.read()["params"]["name"], "stopped");
assert!(p.child.wait().unwrap().success());
assert_eq!(std::fs::read(dir.join("stopped")).unwrap(), b"yes");
let mut p = start(&wasm, &dir);
p.request("start", json!({}));
let first = p.read();
let second = p.read();
let outgoing = if first["method"] == "other.background" {
first
} else {
second
};
assert_eq!(outgoing["method"], "other.background");
p.answer(&outgoing, json!({}));
assert!(p.child.wait().unwrap().success());
let provider_doc = &d["result"];
let imports = root.join("tests/plugins/consumer/imports");
std::fs::create_dir_all(&imports).unwrap();
std::fs::write(
imports.join("probe.json"),
sausage_plugin::serde_json::to_vec_pretty(provider_doc).unwrap(),
)
.unwrap();
let generated = sausage_interface::client_rs("probe", provider_doc).unwrap();
std::fs::write(target.join("client.rs"), generated).unwrap();
assert!(
Command::new("cargo")
.env("CARGO_TARGET_DIR", &target)
.args(["build", "--offline", "--manifest-path"])
.arg(root.join("tests/plugins/consumer/Cargo.toml"))
.args(["--target", "wasm32-wasip2"])
.status()
.unwrap()
.success()
);
let consumer = target.join("wasm32-wasip2/debug/sdk-consumer.wasm");
let mut client = start(&consumer, &dir);
client.request("rpc.discover", json!({}));
let consumer_doc = client.read()["result"].clone();
assert!(client.child.wait().unwrap().success());
let imported = &consumer_doc["x-sausage-imports"]["probe"];
assert!(sausage_interface::compat::check(imported, provider_doc).is_empty());
let mut broken = provider_doc.clone();
broken["methods"]
.as_array_mut()
.unwrap()
.retain(|m| m["name"] != "add");
assert!(!sausage_interface::compat::check(imported, &broken).is_empty());
let mut client = start(&consumer, &dir);
client.request("run", json!({}));
let outgoing = client.read();
assert_eq!(outgoing["method"], "core.call");
assert_eq!(outgoing["params"]["method"], "probe.add");
let mut provider = start(&wasm, &dir);
provider.request("add", outgoing["params"]["params"].clone());
let reply = provider.read();
assert!(provider.child.wait().unwrap().success());
client.send(json!({"jsonrpc":"2.0","method":"core.notify","params":{"call":outgoing["id"],"name":"typed","params":{}}}));
client.answer(&outgoing, reply["result"].clone());
assert_eq!(client.read()["result"], 5);
assert!(client.child.wait().unwrap().success());
let mut provider = start(&wasm, &dir);
provider.request("serve", json!({}));
assert_eq!(provider.read()["result"], Value::Null);
let mut client = start(&consumer, &dir);
client.request("endpoint", json!({}));
assert_eq!(client.read()["result"], 7);
assert!(client.child.wait().unwrap().success());
let mut stderr = String::new();
std::io::Read::read_to_string(client.child.stderr.as_mut().unwrap(), &mut stderr).unwrap();
assert!(stderr.contains("endpoint notification: endpoint-progress"));
let path = dir.join("endpoint");
let original = std::fs::read_to_string(&path).unwrap();
std::fs::write(&path, original.replace("probe-00000000", "probe-11111111")).unwrap();
let mut client = start(&consumer, &dir);
client.request("endpoint", json!({}));
assert!(
client.read()["error"]["message"]
.as_str()
.unwrap()
.contains("identity mismatch")
);
assert!(client.child.wait().unwrap().success());
std::fs::write(path, original).unwrap();
drop(provider.input.take());
assert_eq!(provider.read()["params"]["name"], "stopped");
assert!(provider.child.wait().unwrap().success());
std::fs::remove_dir_all(dir).unwrap();
}