sausage core: modules per topic, journal writer thread with WAL and batches, hash cache by metadata, 49 tests per rule

Blank stdout lines give no warning. core.exit requests the exit in its
own handler.
This commit is contained in:
2026-10-23 00:32:33 +01:00
parent 218a718a3d
commit 8542b9fcb8
17 changed files with 2016 additions and 814 deletions
+217 -45
View File
@@ -1,47 +1,219 @@
//! A raw JSON-RPC guest for host integration tests.
use std::io::{self,BufRead,Write};
use serde_json::{Value,json};
fn send(v:Value) { let mut out=io::stdout().lock();writeln!(out,"\n{v}").unwrap();out.flush().unwrap(); }
//! Raw JSON-RPC methods used by the host's protocol tests.
use serde_json::{Value, json};
use std::io::{self, BufRead, Read, Write};
type Lines = io::Lines<io::BufReader<io::Stdin>>;
fn send(value: Value) {
let mut output = io::stdout().lock();
writeln!(output, "\n{value}").unwrap();
output.flush().unwrap();
}
fn receive(lines: &mut Lines) -> Value {
serde_json::from_str(&lines.next().unwrap().unwrap()).unwrap()
}
fn echo(params: &Value) -> Value {
params.clone()
}
fn environment() -> Value {
json!({
"pid": std::env::var("SAUSAGE_PID").unwrap(),
"plugin": std::env::var("SAUSAGE_PLUGIN").unwrap(),
"parent": std::env::var("SAUSAGE_PARENT").unwrap(),
})
}
fn multicall(lines: &mut Lines) -> Value {
send(json!({
"jsonrpc": "2.0",
"id": 1,
"method": "probe.echo",
"params": {"n": 1}
}));
send(json!({
"jsonrpc": "2.0",
"id": "1",
"method": "probe.echo",
"params": {"n": 2}
}));
for _ in 0..2 {
let response = receive(lines);
if response["id"] == 1 {
assert_eq!(response["result"]["n"], 1);
} else {
assert_eq!(response["id"], "1");
assert_eq!(response["result"]["n"], 2);
}
}
json!({"count":2})
}
fn notification(params: &Value) -> Value {
send(json!({
"jsonrpc": "2.0",
"method": "probe.write",
"params": params
}));
for _ in 0..200 {
let contents = std::fs::read_to_string(params["path"].as_str().unwrap()).ok();
if contents.as_deref() == params["text"].as_str() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
json!({})
}
fn unknown_id() {
send(json!({
"jsonrpc": "2.0",
"id": "late",
"result": 7
}));
}
fn wait_eof(lines: &mut Lines) {
for _ in lines {}
std::fs::write("/state/stopped", "yes").unwrap();
}
fn busy() -> Value {
loop {
std::hint::spin_loop();
}
}
fn sleep(params: &Value) -> Value {
let duration = std::time::Duration::from_millis(params["ms"].as_u64().unwrap_or(100));
std::thread::sleep(duration);
json!({})
}
fn exit_without_answer() {
eprintln!("last line");
}
fn junk() -> Value {
io::stdout().write_all(&[255, 10]).unwrap();
println!("junk\n{{}}\n[]");
eprintln!("stderr line");
json!({})
}
fn invalid_utf8() -> Value {
io::stdout().write_all(&[255, 10]).unwrap();
json!({})
}
fn blank() -> Value {
println!("\n \t\n");
json!({})
}
fn notify() -> Value {
send(json!({
"jsonrpc": "2.0",
"method": "core.notify",
"params": {
"name": "progress",
"params": {"n": 1}
}
}));
std::thread::sleep(std::time::Duration::from_millis(30));
json!({})
}
fn serve(request: &Value, lines: &mut Lines) {
send(json!({
"jsonrpc": "2.0",
"id": request["id"],
"result": std::env::var("SAUSAGE_PID").unwrap()
}));
for _ in lines {}
}
fn network(params: &Value) -> Value {
let mut socket = std::net::TcpStream::connect(params["tcp"].as_str().unwrap()).unwrap();
let mut text = String::new();
socket.read_to_string(&mut text).unwrap();
let udp = std::net::UdpSocket::bind("127.0.0.1:0").unwrap();
udp.send_to(b"udp", params["udp"].as_str().unwrap())
.unwrap();
json!(text)
}
fn read_file(params: &Value) -> Value {
json!(std::fs::read_to_string(params["path"].as_str().unwrap()).unwrap())
}
fn write_file(params: &Value) -> Value {
std::fs::write(
params["path"].as_str().unwrap(),
params["text"].as_str().unwrap(),
)
.unwrap();
json!({})
}
fn outgoing_call(params: &Value, lines: &mut Lines) -> Value {
send(json!({
"jsonrpc": "2.0",
"id": {"nested": 7},
"method": params["method"],
"params": params["params"]
}));
loop {
let response = receive(lines);
if response["id"] == json!({"nested":7}) {
return response["result"].clone();
}
}
}
fn lock(params: &Value, lines: &mut Lines) -> Value {
outgoing_call(&json!({"method":"core.lock", "params":params}), lines)
}
fn dispatch(request: &Value, lines: &mut Lines) -> Option<Value> {
let params = &request["params"];
let result = match request["method"].as_str().unwrap() {
"echo" | "rpc.discover" => echo(params),
"env" => environment(),
"multicall" => multicall(lines),
"notification" => notification(params),
"unknownid" => {
unknown_id();
return None;
}
"request_id" => request["id"].clone(),
"wait_eof" => {
wait_eof(lines);
return None;
}
"busy" => busy(),
"sleep" => sleep(params),
"exit" => {
exit_without_answer();
return None;
}
"junk" => junk(),
"blank" => blank(),
"invalid_utf8" => invalid_utf8(),
"notify" => notify(),
"serve" => {
serve(request, lines);
return None;
}
"network" => network(params),
"read" => read_file(params),
"write" => write_file(params),
"call" => outgoing_call(params, lines),
"lock" => lock(params, lines),
_ => {
send(json!({
"jsonrpc": "2.0",
"id": request["id"],
"error": {
"code": -32601,
"message": "method not found"
}
}));
return None;
}
};
Some(result)
}
fn main() {
let mut lines=io::stdin().lock().lines();
let request:Value=serde_json::from_str(&lines.next().unwrap().unwrap()).unwrap();
let id=request.get("id").cloned();let p=&request["params"];
let result=match request["method"].as_str().unwrap() {
"echo"|"rpc.discover"=>p.clone(),
"env"=>json!({"pid":std::env::var("SAUSAGE_PID").unwrap(),"plugin":std::env::var("SAUSAGE_PLUGIN").unwrap(),"parent":std::env::var("SAUSAGE_PARENT").unwrap()}),
"multicall"=>{
send(json!({"jsonrpc":"2.0","id":1,"method":"probe.echo","params":{"n":1}}));
send(json!({"jsonrpc":"2.0","id":"1","method":"probe.echo","params":{"n":2}}));
let mut seen=Vec::new();while seen.len()<2 {let r:Value=serde_json::from_str(&lines.next().unwrap().unwrap()).unwrap();if r["id"]==1 {assert_eq!(r["result"]["n"],1);} else {assert_eq!(r["id"],"1");assert_eq!(r["result"]["n"],2);}seen.push(r["id"].clone());}json!({"count":seen.len()})
},
"notification"=>{
send(json!({"jsonrpc":"2.0","method":"probe.write","params":p}));
for _ in 0..200 {if std::fs::exists(p["path"].as_str().unwrap()).unwrap() {break;}std::thread::sleep(std::time::Duration::from_millis(10));}json!({})
},
"unknownid"=>{send(json!({"jsonrpc":"2.0","id":"late","result":7}));return;},
"request_id"=>request["id"].clone(),
"wait_eof"=>{for _ in lines {} std::fs::write("/state/stopped","yes").unwrap(); return;},
"busy"=>loop {std::hint::spin_loop();},
"sleep"=>{std::thread::sleep(std::time::Duration::from_millis(p["ms"].as_u64().unwrap_or(100)));json!({})},
"exit"=>{eprintln!("last line");return;},
"junk"=>{io::stdout().write_all(&[255,10]).unwrap();println!("junk\n{{}}\n[]");eprintln!("stderr line");json!({})},
"notify"=>{send(json!({"jsonrpc":"2.0","method":"core.notify","params":{"name":"progress","params":{"n":1}}}));std::thread::sleep(std::time::Duration::from_millis(30));json!({})},
"serve"=>{send(json!({"jsonrpc":"2.0","id":id,"result":std::env::var("SAUSAGE_PID").unwrap()}));for _ in lines {} return;},
"network"=>{
use std::io::Read;
let mut socket=std::net::TcpStream::connect(p["tcp"].as_str().unwrap()).unwrap();
let mut text=String::new();socket.read_to_string(&mut text).unwrap();
let udp=std::net::UdpSocket::bind("127.0.0.1:0").unwrap();udp.send_to(b"udp",p["udp"].as_str().unwrap()).unwrap();json!(text)
},
"read"=>json!(std::fs::read_to_string(p["path"].as_str().unwrap()).unwrap()),
"write"=>{std::fs::write(p["path"].as_str().unwrap(),p["text"].as_str().unwrap()).unwrap();json!({})},
"call"|"lock"=>{
let method=if request["method"]=="lock" {"core.lock"} else {p["method"].as_str().unwrap()};
let params=if request["method"]=="lock" {p.clone()} else {p["params"].clone()};
send(json!({"jsonrpc":"2.0","id":{"nested":7},"method":method,"params":params}));
loop {let response:Value=serde_json::from_str(&lines.next().unwrap().unwrap()).unwrap();if response["id"]==json!({"nested":7}) {break response["result"].clone();}}
},
_=>{send(json!({"jsonrpc":"2.0","id":id,"error":{"code":-32601,"message":"method not found"}}));return;}
};
if let Some(id)=id {send(json!({"jsonrpc":"2.0","id":id,"result":result}));}
let mut lines = io::BufReader::new(io::stdin()).lines();
let request = receive(&mut lines);
if let Some(result) = dispatch(&request, &mut lines)
&& let Some(id) = request.get("id")
{
send(json!({
"jsonrpc": "2.0",
"id": id,
"result": result
}));
}
}