Initial Dever 0.0.1

This commit is contained in:
Codex committed 2026-11-03 03:32:07 +00:00
commit 81233d098c
1479 files changed
+163782

No files matched your search

+228
View File
@@ -0,0 +1,228 @@
//! 真实命令的字节管道、非零退出、超时和输出预算;所有写入都在当前测试目录。
use super::{Asset, Bundle, Directory, Limits, Runtime};
use dever_runtime::worker::command::{self, Limits as CommandLimits};
use sha2::{Digest, Sha256};
use std::{fs, path::PathBuf, process::Command, time::Duration};
#[test]
fn command_runs_owned_native_fixture_without_shell_and_drains_on_failure() {
let _execution = super::EXECUTOR.lock().unwrap();
let directory = Directory::new();
let fixture = directory.0.join("fixture");
let source = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../../test/rust/runtime/worker/fixture.rs");
let result = Command::new("rustup")
.args(["run", "1.95.0", "rustc", "--edition=2024", "-O"])
.arg(source)
.arg("-o")
.arg(&fixture)
.output()
.unwrap();
assert!(
result.status.success(),
"{}",
String::from_utf8_lossy(&result.stderr)
);
let runner = directory.0.join("runner");
fs::copy(std::env::current_exe().unwrap(), &runner).unwrap();
let result = Command::new(runner)
.args(["--exact", "command::owned_command_case", "--nocapture"])
.env_clear()
.env("DEVER_COMMAND_FIXTURE", fixture)
.output()
.unwrap();
assert!(
result.status.success(),
"{}\n{}",
String::from_utf8_lossy(&result.stdout),
String::from_utf8_lossy(&result.stderr)
);
}
#[test]
fn owned_command_case() {
let Some(fixture) = std::env::var_os("DEVER_COMMAND_FIXTURE") else {
return;
};
let bytes = fs::read(&fixture).unwrap();
fs::remove_file(fixture).unwrap();
let entry = "module/demo/tool/command";
let launch = serde_json::to_vec(&serde_json::json!({"format":"dever-command-launch-v1","ecosystem":"command","entry":entry,"executable":entry,"arguments":[],"working_directory":"module/demo/tool"})).unwrap();
let files = [
(entry, bytes, true),
("module/demo/tool/command.dever-command.json", launch, false),
]
.map(|(path, bytes, executable)| Asset {
path,
sha256: Box::leak(format!("{:x}", Sha256::digest(&bytes)).into_boxed_str()),
bytes: Box::leak(bytes.into_boxed_slice()),
executable,
});
let bundle: &'static Bundle = Box::leak(Box::new(Bundle {
files: Box::leak(Box::new(files)),
}));
let root = std::env::current_exe()
.unwrap()
.parent()
.unwrap()
.to_owned();
let runtime = Runtime::new(Limits {
threads: 2,
workers: 1,
tasks: 32,
})
.unwrap();
runtime
.run(async |scope| {
let arguments = ["--echo", "a b", "$(touch never)", "\"quoted\""].map(str::to_owned);
let output = command::run(
scope,
bundle,
entry,
&arguments,
Some(&[0, 255, 1]),
CommandLimits::default(),
)
.await
.unwrap();
assert_eq!(output.code, 7);
assert_eq!(output.stdout, [0, 255, 1]);
assert_eq!(output.stderr, b"a b\n$(touch never)\n\"quoted\"");
let empty = command::run(
scope,
bundle,
entry,
&["--echo".into()],
None,
CommandLimits::default(),
)
.await
.unwrap();
assert!(empty.stdout.is_empty());
let error = command::run(
scope,
bundle,
entry,
&["--flood".into()],
None,
CommandLimits {
timeout_ms: 1000,
output_limit: 100,
},
)
.await
.err()
.unwrap();
assert!(error.contains("limit"));
let marker = root.join("timed-pid");
let error = command::run(
scope,
bundle,
entry,
&["--hang".into(), marker.to_str().unwrap().into()],
None,
CommandLimits {
timeout_ms: 100,
output_limit: 100,
},
)
.await
.err()
.unwrap();
assert!(error.contains("timed out"));
let pid = fs::read_to_string(marker).unwrap();
assert!(!PathBuf::from(format!("/proc/{pid}")).exists());
let marker = root.join("cancelled-pid");
let arguments = ["--hang".into(), marker.to_str().unwrap().into()];
let running = command::run(
scope,
bundle,
entry,
&arguments,
None,
CommandLimits::default(),
);
tokio::pin!(running);
tokio::select! {
_ = &mut running => panic!("hanging command completed before cancellation"),
ready = tokio::time::timeout(Duration::from_secs(3), async {
while !marker.is_file() { tokio::time::sleep(Duration::from_millis(5)).await; }
}) => ready.unwrap(),
}
})
.unwrap();
let pid = fs::read_to_string(root.join("cancelled-pid")).unwrap();
assert!(!PathBuf::from(format!("/proc/{pid}")).exists());
runtime
.run(async |scope| {
let (started, ready) = tokio::sync::oneshot::channel();
let (release, receive) = std::sync::mpsc::channel();
let blocker = scope
.spawn_sync(move || {
started.send(()).unwrap();
receive.recv().unwrap();
})
.unwrap();
ready.await.unwrap();
let queued = scope
.spawn(move |child| async move {
command::run(
&child,
bundle,
entry,
&["--echo".into()],
None,
CommandLimits::default(),
)
.await
})
.unwrap();
let stopped =
tokio::time::timeout(Duration::from_millis(300), queued.timeout(30)).await;
release.send(()).unwrap();
blocker.wait().await.unwrap();
assert!(matches!(
stopped,
Ok(Ok(dever_runtime::task::Timed::Expired(None)))
));
worker_finish(scope).await;
})
.unwrap();
runtime
.run(async |scope| {
let marker = root.join("child-cancelled-pid");
let arguments = vec!["--hang".into(), marker.to_str().unwrap().into()];
let child = scope
.spawn(move |child| async move {
command::run(
&child,
bundle,
entry,
&arguments,
None,
CommandLimits::default(),
)
.await
})
.unwrap();
tokio::time::timeout(Duration::from_secs(3), async {
while !marker.is_file() {
tokio::time::sleep(Duration::from_millis(5)).await;
}
})
.await
.unwrap();
child.stop().await.unwrap();
let pid = fs::read_to_string(marker).unwrap();
assert!(!PathBuf::from(format!("/proc/{pid}")).exists());
worker_finish(scope).await;
})
.unwrap();
}
async fn worker_finish(scope: &dever_runtime::task::Scope<'_>) {
dever_runtime::worker::finish(scope, Ok(())).await.unwrap();
}
+181
View File
@@ -0,0 +1,181 @@
//! 独立 std-only 协议对端;测试从固定 Rust 工具链构建,不调用宿主语言环境。
use std::io::{Read, Write};
fn read() -> Option<String> {
let mut prefix = [0; 4];
std::io::stdin().read_exact(&mut prefix).ok()?;
let mut bytes = vec![0; u32::from_be_bytes(prefix) as usize];
std::io::stdin().read_exact(&mut bytes).unwrap();
Some(String::from_utf8(bytes).unwrap())
}
fn send(value: &str) {
let mut output = std::io::stdout().lock();
output
.write_all(&(value.len() as u32).to_be_bytes())
.unwrap();
output.write_all(value.as_bytes()).unwrap();
output.flush().unwrap();
}
fn main() {
let args = std::env::args().skip(1).collect::<Vec<_>>();
if let Some(mode) = args.first() {
match mode.as_str() {
"--echo" => {
let mut bytes = Vec::new();
std::io::stdin().read_to_end(&mut bytes).unwrap();
std::io::stdout().write_all(&bytes).unwrap();
std::io::stderr()
.write_all(args[1..].join("\n").as_bytes())
.unwrap();
std::process::exit(7);
}
"--hang" => {
std::fs::write(&args[1], std::process::id().to_string()).unwrap();
std::thread::sleep(std::time::Duration::from_secs(10));
return;
}
"--flood" => {
std::io::stdout().write_all(&[1; 4096]).unwrap();
std::io::stderr().write_all(&[2; 4096]).unwrap();
return;
}
_ => panic!("unknown test command"),
}
}
let Some(hello) = read() else {
return;
};
assert!(hello.contains("\"kind\":\"hello\""));
let (fields, setting) = hello.rsplit_once(",\"setting\":").unwrap();
if setting.contains("sensitive") {
assert!(setting.contains("sensitive\\\"\\\\雪"));
}
send(&format!("{fields}}}").replace("\"kind\":\"hello\"", "\"kind\":\"ready\""));
let health = read().unwrap();
assert!(health.contains("\"kind\":\"health\""));
send(&health);
let mut bad_shutdown = false;
while let Some(request) = read() {
if request == "{\"kind\":\"shutdown\"}" {
if bad_shutdown {
send("{}");
std::process::exit(7);
}
send(&request);
return;
}
let id = request
.split("\"id\":")
.nth(1)
.unwrap()
.split([',', '}'])
.next()
.unwrap();
if request.contains("\"kind\":\"cancel\"") {
send(&format!(
"{{\"kind\":\"error\",\"id\":{id},\"error\":\"dever.cancelled\",\"payload\":null}}"
));
continue;
}
if request.contains("\"operation\":\"measure\"") {
let payload = request
.split_once(",\"payload\":")
.unwrap()
.1
.strip_suffix('}')
.unwrap();
let length = payload.len();
let checksum: u64 = payload.bytes().map(u64::from).sum();
send(&format!(
"{{\"kind\":\"result\",\"id\":{id},\"payload\":{{\"length\":{length},\"checksum\":{checksum}}}}}"
));
continue;
}
if request.contains("\"operation\":\"crash\"") {
std::process::exit(7);
}
if request.contains("\"operation\":\"hang\"") {
continue;
}
if request.contains("\"operation\":\"ignore\"") {
std::thread::sleep(std::time::Duration::from_secs(10));
continue;
}
if request.contains("\"operation\":\"shutdown_bad\"") {
bad_shutdown = true;
}
if request.contains("\"operation\":\"native\"") {
if request.contains("\"input\":-4") || request.contains("\"input\":-5") {
bad_shutdown = true;
}
if request.contains("\"input\":-1") {
send(&format!(
"{{\"kind\":\"result\",\"id\":{id},\"payload\":{{\"value\":\"wrong-type\"}}}}"
));
continue;
}
if request.contains("\"input\":-2")
|| request.contains("\"input\":-3")
|| request.contains("\"input\":-5")
{
let error = if request.contains("\"input\":-2") {
"not.declared.Failed"
} else {
"dever.api.Error.Invalid"
};
send(&format!(
"{{\"kind\":\"error\",\"id\":{id},\"error\":\"{error}\",\"payload\":{{}}}}"
));
continue;
}
let value = if request.contains("\"input\":99}") {
std::process::id()
} else {
42
};
send(&format!(
"{{\"kind\":\"result\",\"id\":{id},\"payload\":{{\"value\":{value}}}}}"
));
continue;
}
if request.contains("\"operation\":\"bad\"") {
send("{\"kind\":\"result\",\"id\":900,\"payload\":{}}");
continue;
}
if request.contains("\"operation\":\"error\"") {
send(&format!(
"{{\"kind\":\"error\",\"id\":{id},\"error\":\"demo.test.Error.Failed\",\"payload\":{{\"message\":\"failed\"}}}}"
));
continue;
}
if let Some((_, fields)) = [
("null_error", "\"kind\":\"result\",\"error\":null"),
("missing_error", "\"kind\":\"error\""),
("extra_field", "\"kind\":\"result\",\"unexpected\":true"),
]
.into_iter()
.find(|(operation, _)| request.contains(&format!("\"operation\":\"{operation}\"")))
{
send(&format!("{{{fields},\"id\":{id},\"payload\":null}}"));
continue;
}
let response = format!(
"{{\"kind\":\"result\",\"id\":{id},\"payload\":{{\"value\":42,\"pid\":{}}}}}",
std::process::id()
);
if request.contains("\"operation\":\"fragment\"") {
let prefix = (response.len() as u32).to_be_bytes();
std::io::stdout().write_all(&prefix[..2]).unwrap();
std::io::stdout().flush().unwrap();
assert!(read().unwrap().contains("\"kind\":\"cancel\""));
std::io::stdout().write_all(&prefix[2..]).unwrap();
std::io::stdout().write_all(response.as_bytes()).unwrap();
std::io::stdout().flush().unwrap();
} else {
send(&response);
}
}
}
+259
View File
@@ -0,0 +1,259 @@
//! 逐调用取消与校验失败必须完成自己的清理,不等待另一个调用或阻塞任务。
use super::{CONTRACT, Directory, ENTRY, EXECUTOR, bundle, call, pid};
use dever_runtime::{
task::{Limits, Runtime, Timed},
wire::json::Json,
worker::{self, Setting},
};
use std::{fs, path::PathBuf, process::Command, time::Duration};
#[test]
fn worker_validation_cancellation_and_cleanup_are_owned() {
let _execution = EXECUTOR.lock().unwrap();
let directory = Directory::new();
let fixture = directory.0.join("fixture");
let source = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../../test/rust/runtime/worker/fixture.rs");
let built = Command::new("rustup")
.args(["run", "1.95.0", "rustc", "--edition=2024", "-O"])
.arg(source)
.arg("-o")
.arg(&fixture)
.output()
.unwrap();
assert!(
built.status.success(),
"{}",
String::from_utf8_lossy(&built.stderr)
);
let runner = directory.0.join("runner");
fs::copy(std::env::current_exe().unwrap(), &runner).unwrap();
let result = Command::new(runner)
.args(["--exact", "lifecycle::owned_lifecycle_case", "--nocapture"])
.env_clear()
.env("DEVER_LIFECYCLE_FIXTURE", fixture)
.output()
.unwrap();
assert!(
result.status.success(),
"{}\n{}",
String::from_utf8_lossy(&result.stdout),
String::from_utf8_lossy(&result.stderr)
);
}
#[test]
fn owned_lifecycle_case() {
let Some(fixture) = std::env::var_os("DEVER_LIFECYCLE_FIXTURE") else {
return;
};
let bundle = bundle(fs::read(&fixture).unwrap());
fs::remove_file(fixture).unwrap();
let runtime = Runtime::new(Limits {
threads: 2,
workers: 1,
tasks: 32,
})
.unwrap();
runtime
.run(async |scope| {
let first = pid(call(scope, bundle, "echo").await.unwrap());
let held = worker::call(
scope,
bundle,
ENTRY,
CONTRACT,
&Setting::empty(),
"echo",
Json::parse("{}").unwrap(),
)
.await
.unwrap();
let queued = scope
.spawn(move |child| async move { call(&child, bundle, "echo").await })
.unwrap();
let stopped =
tokio::time::timeout(Duration::from_millis(300), queued.timeout(30)).await;
assert!(matches!(stopped, Ok(Ok(Timed::Expired(None)))));
assert_eq!(first, pid(held.resolve(Ok).await.unwrap()));
let held = worker::call(
scope,
bundle,
ENTRY,
CONTRACT,
&Setting::empty(),
"echo",
Json::parse("{}").unwrap(),
)
.await
.unwrap();
let queued = scope
.spawn(move |child| async move { call(&child, bundle, "echo").await })
.unwrap();
tokio::time::sleep(Duration::from_millis(30)).await;
let failed = held
.resolve::<()>(|_| Err("invalid typed payload".into()))
.await
.unwrap_err();
assert!(failed.contains("invalid typed payload"));
assert!(!PathBuf::from(format!("/proc/{first}")).exists());
assert!(
queued
.wait()
.await
.unwrap()
.err()
.unwrap()
.contains("queued")
);
let second = pid(call(scope, bundle, "echo").await.unwrap());
assert_ne!(first, second);
let held = worker::call(
scope,
bundle,
ENTRY,
CONTRACT,
&Setting::empty(),
"echo",
Json::parse("{}").unwrap(),
)
.await
.unwrap();
let queued = scope
.spawn(move |child| async move { call(&child, bundle, "echo").await })
.unwrap();
tokio::time::sleep(Duration::from_millis(30)).await;
drop(held);
let failed = tokio::time::timeout(Duration::from_secs(3), queued.wait())
.await
.unwrap()
.unwrap()
.err()
.unwrap();
assert!(failed.contains("queued"));
assert!(!PathBuf::from(format!("/proc/{second}")).exists());
let third = pid(call(scope, bundle, "echo").await.unwrap());
assert_ne!(second, third);
let panicked = scope
.spawn(move |child| async move {
worker::call(
&child,
bundle,
ENTRY,
CONTRACT,
&Setting::empty(),
"echo",
Json::parse("{}").unwrap(),
)
.await
.unwrap()
.resolve::<()>(|_| panic!("typed decoder panicked"))
.await
})
.unwrap();
assert!(matches!(
tokio::time::timeout(Duration::from_secs(3), panicked.wait()).await,
Ok(Err(dever_runtime::task::Error::Panicked))
));
assert!(!PathBuf::from(format!("/proc/{third}")).exists());
worker::finish(scope, Ok(())).await.unwrap();
})
.unwrap();
runtime
.run(async |scope| {
#[derive(serde::Deserialize)]
struct Config {
#[serde(rename = "token")]
_token: dever_runtime::value::secret::Secret,
}
let document = dever_runtime::adapter::Document::parse(
r#"{"adapter":{"demo.test":{"setting":{"token":"sensitive\"\\雪"}}}}"#.into(),
)
.unwrap();
let schemas = [dever_runtime::adapter::Schema {
name: "demo.test",
implementations: &["default"],
}];
let selected = document.select(&schemas).unwrap();
let setting = selected[0].worker_setting::<Config>().unwrap();
let input = Json::parse("{}").unwrap();
let process = pid(worker::call(
scope,
bundle,
ENTRY,
CONTRACT,
&setting,
"echo",
input.clone(),
)
.await
.unwrap()
.resolve(Ok)
.await
.unwrap());
let error = worker::call(scope, bundle, ENTRY, CONTRACT, &setting, "bad", input)
.await
.err()
.unwrap();
assert!(!error.contains("sensitive"));
assert!(!PathBuf::from(format!("/proc/{process}")).exists());
worker::finish(scope, Ok(())).await.unwrap();
})
.unwrap();
runtime
.run(async |scope| {
let (started, ready) = tokio::sync::oneshot::channel();
let (release, receive) = std::sync::mpsc::channel();
let blocking = scope
.spawn_sync(move || {
started.send(()).unwrap();
receive.recv().unwrap();
})
.unwrap();
ready.await.unwrap();
let queued = scope
.spawn(move |child| async move { call(&child, bundle, "echo").await })
.unwrap();
let cancelled =
tokio::time::timeout(Duration::from_millis(300), queued.timeout(30)).await;
release.send(()).unwrap();
blocking.wait().await.unwrap();
assert!(matches!(cancelled, Ok(Ok(Timed::Expired(None)))));
worker::finish(scope, Ok(())).await.unwrap();
})
.unwrap();
runtime
.run(async |scope| {
let process = pid(call(scope, bundle, "echo").await.unwrap());
let hanging = scope
.spawn(move |child| async move { call(&child, bundle, "ignore").await })
.unwrap();
let result = tokio::time::timeout(Duration::from_secs(3), hanging.timeout(50))
.await
.unwrap()
.unwrap();
assert!(matches!(result, Timed::Expired(None)));
assert!(!PathBuf::from(format!("/proc/{process}")).exists());
worker::finish(scope, Ok(())).await.unwrap();
})
.unwrap();
runtime
.run(async |scope| {
let process = pid(call(scope, bundle, "shutdown_bad").await.unwrap());
let error = worker::finish::<()>(scope, Err("primary failure".into()))
.await
.unwrap_err();
assert!(
error.contains("primary failure") && error.contains("shutdown"),
"{error}"
);
assert!(!PathBuf::from(format!("/proc/{process}")).exists());
})
.unwrap();
}
+252
View File
@@ -0,0 +1,252 @@
//! 真实受管 Worker 与隔离资源目录;不访问已安装服务或生产缓存。
use dever_runtime::{
task::{Limits, Runtime},
wire::json::Json,
worker::{self, Asset, Bundle, Reply, Setting},
};
use sha2::{Digest, Sha256};
use std::{
fs,
os::unix::fs::{DirBuilderExt, PermissionsExt},
path::PathBuf,
process::Command,
time::Duration,
};
const ENTRY: &str = "module/demo/test/worker";
static EXECUTOR: std::sync::Mutex<()> = std::sync::Mutex::new(());
mod command;
mod lifecycle;
mod performance;
const CONTRACT: &str = r#"{"port":"demo.test","adapter":"default","ecosystem":"exec","entry":"module/demo/test/worker","schema":"owned-fixture","capabilities":[],"operations":["bad","crash","echo","error","extra_field","fragment","hang","ignore","measure","missing_error","null_error","shutdown_bad"],"inputs":{},"outputs":{},"errors":{"demo.test.Error.Failed":{}},"setting":null}"#;
struct Directory(PathBuf);
impl Directory {
fn new() -> Self {
let path = std::env::temp_dir().join(format!(
"dever-worker-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
fs::DirBuilder::new().mode(0o700).create(&path).unwrap();
Self(path)
}
}
impl Drop for Directory {
fn drop(&mut self) {
fs::remove_dir_all(&self.0).unwrap();
}
}
fn bundle(bytes: Vec<u8>) -> &'static Bundle {
let launch = serde_json::to_vec(&serde_json::json!({"format":"dever-worker-launch-v1","ecosystem":"exec","entry":ENTRY,"executable":ENTRY,"arguments":[],"working_directory":"module/demo/test"})).unwrap();
let files = [
(ENTRY, bytes, true),
("module/demo/test/worker.dever-worker.json", launch, false),
]
.map(|(path, bytes, executable)| Asset {
path,
sha256: Box::leak(format!("{:x}", Sha256::digest(&bytes)).into_boxed_str()),
bytes: Box::leak(bytes.into_boxed_slice()),
executable,
});
Box::leak(Box::new(Bundle {
files: Box::leak(Box::new(files)),
}))
}
fn pid(reply: Reply) -> u32 {
let Reply::Result(value) = reply else {
panic!("expected result")
};
let value: serde_json::Value = serde_json::from_str(&value).unwrap();
assert_eq!(value["value"], 42);
value["pid"].as_u64().unwrap() as u32
}
async fn call(
scope: &dever_runtime::task::Scope<'_>,
bundle: &'static Bundle,
operation: &str,
) -> Result<Reply, String> {
worker::call(
scope,
bundle,
ENTRY,
CONTRACT,
&Setting::empty(),
operation,
Json::parse("{}").unwrap(),
)
.await?
.resolve(Ok)
.await
}
#[test]
fn real_worker_runs_from_a_relocated_owned_executable() {
let _execution = EXECUTOR.lock().unwrap();
let directory = Directory::new();
let fixture = directory.0.join("fixture");
let source = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../../test/rust/runtime/worker/fixture.rs");
let result = Command::new("rustup")
.args(["run", "1.95.0", "rustc", "--edition=2024", "-O"])
.arg(source)
.arg("-o")
.arg(&fixture)
.output()
.unwrap();
assert!(
result.status.success(),
"{}",
String::from_utf8_lossy(&result.stderr)
);
let runner = directory.0.join("runner");
fs::copy(std::env::current_exe().unwrap(), &runner).unwrap();
let result = Command::new(runner)
.args(["--exact", "owned_process_protocol_case", "--nocapture"])
.env_clear()
.env("DEVER_WORKER_FIXTURE", &fixture)
.output()
.unwrap();
assert!(
result.status.success(),
"{}\n{}",
String::from_utf8_lossy(&result.stdout),
String::from_utf8_lossy(&result.stderr)
);
}
#[test]
fn owned_process_protocol_case() {
let Some(fixture) = std::env::var_os("DEVER_WORKER_FIXTURE") else {
return;
};
let bundle = bundle(fs::read(&fixture).unwrap());
fs::remove_file(fixture).unwrap();
let runtime = Runtime::new(Limits {
threads: 2,
workers: 1,
tasks: 32,
})
.unwrap();
let process = runtime
.run(async |scope| {
let first = pid(call(scope, bundle, "echo").await.unwrap());
assert_eq!(first, pid(call(scope, bundle, "echo").await.unwrap()));
for source in [
"null",
" \n{\"text\":\"雪\\u96ea\\n🙂\",\"value\":1e+02} \t",
] {
let response = worker::call(
scope,
bundle,
ENTRY,
CONTRACT,
&Setting::empty(),
"measure",
Json::parse(source).unwrap(),
)
.await
.unwrap()
.resolve(Ok)
.await
.unwrap();
let Reply::Result(payload) = response else {
panic!("expected measured result")
};
let measured: serde_json::Value = serde_json::from_str(&payload).unwrap();
let expected = source.trim_ascii();
assert_eq!(measured["length"], expected.len());
assert_eq!(
measured["checksum"],
expected.bytes().map(u64::from).sum::<u64>()
);
}
let Reply::Error { identity, payload } = call(scope, bundle, "error").await.unwrap()
else {
panic!("business error")
};
assert_eq!(identity, "demo.test.Error.Failed");
assert_eq!(payload, "{\"message\":\"failed\"}");
for operation in ["hang", "fragment"] {
assert!(
tokio::time::timeout(Duration::from_millis(30), call(scope, bundle, operation))
.await
.is_err()
);
assert_eq!(first, pid(call(scope, bundle, "echo").await.unwrap()));
}
assert!(call(scope, bundle, "crash").await.is_err());
let second = pid(call(scope, bundle, "echo").await.unwrap());
assert_ne!(first, second);
assert!(call(scope, bundle, "bad").await.is_err());
let third = pid(call(scope, bundle, "echo").await.unwrap());
assert_ne!(second, third);
worker::finish(scope, Ok(third)).await.unwrap()
})
.unwrap();
assert!(!PathBuf::from(format!("/proc/{process}")).exists());
for operation in ["null_error", "missing_error", "extra_field"] {
runtime
.run(async |scope| {
let process = pid(call(scope, bundle, "echo").await.unwrap());
assert!(call(scope, bundle, operation).await.is_err());
assert!(!PathBuf::from(format!("/proc/{process}")).exists());
worker::finish(scope, Ok(())).await.unwrap();
})
.unwrap();
}
}
#[test]
fn resource_publication_reuses_only_exact_owned_bytes_and_modes() {
let directory = Directory::new();
let resources = bundle(b"not executed".to_vec());
fs::create_dir(directory.0.join("data")).unwrap();
fs::set_permissions(directory.0.join("data"), fs::Permissions::from_mode(0o755)).unwrap();
let root = resources.prepare_in(&directory.0).unwrap();
assert_eq!(root, resources.prepare_in(&directory.0).unwrap());
fs::write(root.join("unlocked.py"), b"not part of bundle").unwrap();
assert!(
resources
.prepare_in(&directory.0)
.unwrap_err()
.contains("unexpected")
);
fs::remove_file(root.join("unlocked.py")).unwrap();
fs::set_permissions(root.join(ENTRY), fs::Permissions::from_mode(0o700)).unwrap();
assert!(
resources
.prepare_in(&directory.0)
.unwrap_err()
.contains("mode")
);
fs::write(root.join(ENTRY), b"bad resource").unwrap();
fs::set_permissions(root.join(ENTRY), fs::Permissions::from_mode(0o500)).unwrap();
assert!(resources.prepare_in(&directory.0).is_err());
let duplicate = Bundle {
files: Box::leak(
vec![Asset {
path: "../escape",
bytes: b"",
sha256: "",
executable: false,
}]
.into_boxed_slice(),
),
};
assert!(
duplicate
.prepare_in(&directory.0)
.unwrap_err()
.contains("path")
);
}
+166
View File
@@ -0,0 +1,166 @@
//! 真实复用 Worker 的逐调用成本;输入编码、进程启动和结果核验不计入样本。
use super::{CONTRACT, ENTRY, EXECUTOR, bundle};
use dever_runtime::{
task::{Limits, Runtime, Scope},
wire::json::Json,
worker::{self, Bundle, Reply, Setting},
};
use std::{fs, path::PathBuf, time::Instant};
mod native;
async fn request(scope: &Scope<'_>, bundle: &'static Bundle, input: Json) -> (usize, u64) {
let response = worker::call(
scope,
bundle,
ENTRY,
CONTRACT,
&Setting::empty(),
"measure",
input,
)
.await
.unwrap()
.resolve(Ok)
.await
.unwrap();
let Reply::Result(payload) = response else {
panic!("expected measured result")
};
#[derive(serde::Deserialize)]
struct Output {
length: usize,
checksum: u64,
}
let output: Output = serde_json::from_str(&payload).unwrap();
(output.length, output.checksum)
}
#[test]
#[ignore = "explicit release performance probe"]
fn worker_input_cost() {
let _execution = EXECUTOR.lock().unwrap();
let fixture = PathBuf::from(
std::env::var_os("DEVER_WORKER_BENCH_FIXTURE")
.expect("DEVER_WORKER_BENCH_FIXTURE must name the precompiled Worker fixture"),
);
let bundle = bundle(fs::read(&fixture).unwrap());
let runtime = Runtime::new(Limits {
threads: 1,
workers: 1,
tasks: 32,
})
.unwrap();
runtime
.run(async |scope| {
let mut native = native::Native::start(&fixture, CONTRACT).await.unwrap();
eprintln!(
"worker_input: 15 fixed rounds; one async worker; owned inputs prepared before timing; \
same executable, payload, frame protocol, deadlines and reply checks; \
rust_independent uses Tokio directly without Dever Worker; \
it excludes shared-session admission, caller-cancellation handshake and Scope cleanup; \
no compiler runs in this probe"
);
println!("\ncase,sample,implementation,iterations,elapsed_ns,checksum");
for text in ["null", " \n{\"text\":\"雪\\u96ea\\n🙂\",\"value\":1e+02} \t"] {
let trimmed = text.trim_ascii();
let expected = (trimmed.len(), trimmed.bytes().map(u64::from).sum::<u64>());
assert_eq!(
request(scope, bundle, Json::parse(text).unwrap()).await,
expected
);
assert_eq!(native.request(trimmed).await.unwrap(), expected);
}
for size in [64, 65_536, 1_048_576] {
let iterations = match size {
64 => 4096,
65_536 => 512,
_ => 64,
};
let input = Json::encode(&serde_json::json!({"text": "a".repeat(size)})).unwrap();
let expected = (
input.as_str().len(),
input.as_str().bytes().map(u64::from).sum::<u64>(),
);
for _ in 0..16 {
assert_eq!(request(scope, bundle, input.clone()).await, expected);
assert_eq!(native.request(input.as_str()).await.unwrap(), expected);
}
for sample in 0..15 {
let mut order = ["dever", "rust_independent", "control"];
order.rotate_left(sample % 3);
for label in order {
let mut results = Vec::with_capacity(iterations);
let elapsed = if label == "rust_independent" {
let inputs = (0..iterations)
.map(|_| input.as_str().to_owned())
.collect::<Vec<_>>();
let started = Instant::now();
for payload in inputs {
results.push(native.request(&payload).await.unwrap());
}
started.elapsed().as_nanos()
} else {
let inputs = vec![input.clone(); iterations];
let started = Instant::now();
for payload in inputs {
results.push(request(scope, bundle, payload).await);
}
started.elapsed().as_nanos()
};
assert!(results.iter().all(|&result| result == expected));
let checksum = expected.1 * results.len() as u64;
println!("worker_input_{size},{sample},{label},{iterations},{elapsed},{checksum}");
}
}
}
worker::finish(scope, Ok(())).await.unwrap();
native.finish().await.unwrap();
})
.unwrap();
}
#[test]
#[ignore = "explicit release performance probe"]
fn validated_reply_decode_cost() {
#[derive(Debug, PartialEq, serde::Deserialize)]
#[serde(deny_unknown_fields)]
struct Output {
text: String,
values: Vec<i64>,
}
for size in [64, 4096, 65_536] {
let payload =
serde_json::json!({"text": "a".repeat(size), "values": [1, 2, 3]}).to_string();
// Channel::read 的完整帧验证已完成;只计量其后的载荷解码,不代表 IPC 往返。
let input = Json::parse(&payload).unwrap();
let expected = dever_runtime::wire::decode::<Output>(input.as_str()).unwrap();
assert_eq!(
expected,
serde_json::from_str::<Output>(input.as_str()).unwrap()
);
for sample in 0..15 {
for before in if sample % 2 == 0 {
[true, false]
} else {
[false, true]
} {
let mut checksum = 0;
let started = Instant::now();
for _ in 0..2000 {
let text = std::hint::black_box(input.as_str());
let value = if before {
dever_runtime::wire::decode::<Output>(text).unwrap()
} else {
serde_json::from_str::<Output>(text).unwrap()
};
checksum += std::hint::black_box(&value).text.len() + value.values.len();
}
let elapsed = started.elapsed().as_nanos();
assert_eq!(checksum, 2000 * (size + 3));
let label = if before { "before" } else { "after" };
println!("worker_reply_{size},{sample},{label},{elapsed},{checksum}");
}
}
}
}
@@ -0,0 +1,135 @@
//! 独立 Tokio IPC 下界:同一对端、帧格式、期限和响应校验,不调用 Dever Worker。
//! 直连不包含共享会话排队、调用方取消握手和 Scope 回收,不能据此宣称完整语义追平。
use bytes::Buf;
use serde::{Deserialize, Serialize};
use std::{io, path::Path, process::Stdio, time::Duration};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
process::{Child, ChildStdin, ChildStdout},
};
use zeroize::Zeroizing;
pub(super) struct Native {
child: Child,
input: ChildStdin,
output: ChildStdout,
next: u64,
}
impl Native {
pub(super) async fn start(executable: &Path, contract: &str) -> io::Result<Self> {
let mut child = tokio::process::Command::new(executable)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.kill_on_drop(true)
.spawn()?;
let mut value = Self {
input: child.stdin.take().unwrap(),
output: child.stdout.take().unwrap(),
child,
next: 1,
};
let contract: serde_json::Value = serde_json::from_str(contract)?;
let mut hello = serde_json::json!({"kind":"hello","version":"dever-component-1"});
for name in ["port", "schema", "adapter", "capabilities", "operations"] {
hello[name] = contract[name].clone();
}
let mut expected = hello.clone();
expected["kind"] = "ready".into();
let mut header = serde_json::to_string(&hello)?;
header.pop();
header.push_str(",\"setting\":null}");
value.write(&header, "", "").await?;
let ready: serde_json::Value = serde_json::from_str(&value.read().await?)?;
assert_eq!(ready, expected);
value.write(r#"{"kind":"health","id":0}"#, "", "").await?;
assert_eq!(value.read().await?.as_str(), r#"{"kind":"health","id":0}"#);
Ok(value)
}
async fn write(&mut self, header: &str, payload: &str, suffix: &str) -> io::Result<()> {
let length = header.len() + payload.len() + suffix.len();
if length > 16 * 1024 * 1024 {
return Err(io::Error::other("frame too large"));
}
let prefix = (length as u32).to_be_bytes();
let mut bytes = Buf::chain(prefix.as_slice(), header.as_bytes())
.chain(payload.as_bytes())
.chain(suffix.as_bytes());
tokio::time::timeout(Duration::from_secs(2), async {
self.input.write_all_buf(&mut bytes).await?;
self.input.flush().await
})
.await
.map_err(io::Error::other)?
}
async fn read(&mut self) -> io::Result<Zeroizing<String>> {
tokio::time::timeout(Duration::from_secs(2), async {
let mut prefix = [0; 4];
self.output.read_exact(&mut prefix).await?;
let length = u32::from_be_bytes(prefix) as usize;
if length == 0 || length > 16 * 1024 * 1024 {
return Err(io::Error::other("invalid frame length"));
}
let mut bytes = vec![0; length];
self.output.read_exact(&mut bytes).await?;
String::from_utf8(bytes)
.map(Zeroizing::new)
.map_err(io::Error::other)
})
.await
.map_err(io::Error::other)?
}
pub(super) async fn request(&mut self, payload: &str) -> io::Result<(usize, u64)> {
#[derive(Serialize)]
struct Header<'text> {
kind: &'text str,
id: u64,
operation: &'text str,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Output {
length: usize,
checksum: u64,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Frame {
kind: String,
id: u64,
payload: Output,
}
let id = self.next;
self.next = id
.checked_add(1)
.ok_or_else(|| io::Error::other("request id exhausted"))?;
let mut header = serde_json::to_string(&Header {
kind: "call",
id,
operation: "measure",
})?;
header.pop();
header.push_str(",\"payload\":");
self.write(&header, payload, "}").await?;
let frame: Frame = serde_json::from_str(&self.read().await?)?;
if frame.kind != "result" || frame.id != id {
return Err(io::Error::other("unexpected response"));
}
Ok((frame.payload.length, frame.payload.checksum))
}
pub(super) async fn finish(mut self) -> io::Result<()> {
self.write(r#"{"kind":"shutdown"}"#, "", "").await?;
assert_eq!(self.read().await?.as_str(), r#"{"kind":"shutdown"}"#);
let status = tokio::time::timeout(Duration::from_secs(2), self.child.wait())
.await
.map_err(io::Error::other)??;
assert!(status.success());
Ok(())
}
}
+171
View File
@@ -0,0 +1,171 @@
//! 出站帧保留 JSON 原文,并在写出长度前验证合并后的预算。
use super::*;
use crate::wire::json::Json;
use std::{hint::black_box, process::Stdio, time::Instant};
#[derive(serde::Serialize)]
struct Call<'input> {
kind: &'static str,
id: u64,
operation: &'input str,
}
#[test]
fn outgoing_payload_preserves_spelling_and_combined_limits() {
let header = Call {
kind: "call",
id: 1,
operation: "雪\"\\",
};
for source in ["null", " \n{\"雪\":[1e9999,-0,\"\\u96ea\"]}\t "] {
let value = Json::parse(source).unwrap();
let head = payload_header(
serde_json::to_string(&header).unwrap(),
4,
",\"payload\":",
value.shape().unwrap(),
)
.unwrap();
let frame = format!("{head}{}}}", source.trim_ascii());
#[derive(serde::Serialize)]
struct Reference<'input> {
#[serde(flatten)]
header: &'input Call<'input>,
payload: &'input RawValue,
}
assert_eq!(
frame,
serde_json::to_string(&Reference {
header: &header,
payload: serde_json::from_str(source).unwrap(),
})
.unwrap()
);
crate::wire::raw::validate(&frame).unwrap();
}
for count in [document::MAX_VALUES - 5, document::MAX_VALUES - 4] {
let value = Json::encode(&vec![0; count]).unwrap();
let encoded = payload_header(
serde_json::to_string(&header).unwrap(),
4,
",\"payload\":",
value.shape().unwrap(),
);
assert_eq!(encoded.is_ok(), count == document::MAX_VALUES - 5);
}
for depth in [document::MAX_DEPTH - 1, document::MAX_DEPTH] {
let value = Json::parse(&("[".repeat(depth) + &"]".repeat(depth))).unwrap();
assert_eq!(
payload_header(
serde_json::to_string(&header).unwrap(),
4,
",\"payload\":",
value.shape().unwrap()
)
.is_ok(),
depth < document::MAX_DEPTH
);
}
}
#[test]
fn oversized_frame_does_not_write_a_partial_prefix() {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
runtime.block_on(async {
let mut child = tokio::process::Command::new("cat")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
let mut channel = Channel::new(child.stdin.take().unwrap(), child.stdout.take().unwrap());
let payload = Json::string(&"a".repeat(LIMIT - 2)).unwrap();
let header = Call {
kind: "call",
id: 1,
operation: "measure",
};
assert!(
channel
.send_payload(
serde_json::to_string(&header).unwrap(),
4,
",\"payload\":",
payload.as_str(),
payload.shape().unwrap()
)
.await
.is_err()
);
let control = serde_json::json!({"kind":"health","id":0});
channel.send(&control).await.unwrap();
assert_eq!(channel.read().await.unwrap().as_str(), control.to_string());
drop(channel);
assert!(child.wait().await.unwrap().success());
});
}
fn reference_header(header: &Call<'_>, values: usize, depth: usize) -> Result<String, String> {
if values + 4 > document::MAX_VALUES || depth + 1 > document::MAX_DEPTH {
return Err("JSON budget".into());
}
let mut text = serde_json::to_string(header).map_err(|error| error.to_string())?;
// 与正式通道一样,整帧字节预算在写出长度前检查,不属于帧头编码计时。
text.pop();
text.push_str(",\"payload\":");
Ok(text)
}
#[test]
#[ignore = "explicit release performance probe; header only, not complete IPC"]
fn outgoing_header_cost() {
let header = Call {
kind: "call",
id: 42,
operation: "measure",
};
let shape = document::Shape::new(2, 1);
let expected = reference_header(&header, 2, 1).unwrap();
assert_eq!(
payload_header(
serde_json::to_string(&header).unwrap(),
4,
",\"payload\":",
shape
)
.unwrap(),
expected
);
for sample in 0..15 {
for native in if sample % 2 == 0 {
[false, true]
} else {
[true, false]
} {
let started = Instant::now();
let mut checksum = 0;
for _ in 0..100_000 {
let text = if native {
reference_header(black_box(&header), black_box(2), black_box(1))
} else {
payload_header(
serde_json::to_string(black_box(&header)).unwrap(),
4,
",\"payload\":",
black_box(shape),
)
}
.unwrap();
checksum += black_box(text).len();
}
assert_eq!(checksum, 100_000 * expected.len());
println!(
"worker_header,{sample},{},{},{checksum}",
if native { "rust" } else { "dever" },
started.elapsed().as_nanos()
);
}
}
}