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

@@ -0,0 +1,64 @@
//! 原始 argv 不消费配置参数或命令名;非法 UTF-8 通过语言失败合同返回。
use super::{Fixture, Project, build, write};
use std::{ffi::OsString, os::unix::ffi::OsStringExt, process::Command};
#[test]
fn native_arguments_preserve_all_values_and_reject_invalid_utf8() {
let fixture = Fixture::new(&[]);
let root = &fixture.directory.0;
write(
root,
"arguments.dever",
"arguments() (values: List<Text>) { values = dever.process.arguments() }\n",
);
let project = Project::load(&root.join("arguments.dever")).unwrap();
let program = project.check().unwrap();
let binary = build::build(
&program,
&project.sources,
"arguments",
&root.join("arguments"),
)
.unwrap();
for arguments in [
vec![],
vec![
"--config",
"配置.json",
"demo.run",
"",
"a b",
"$(touch never)",
"\"quoted\"",
"雪☃",
],
] {
let result = Command::new(&binary)
.args(&arguments)
.env_clear()
.output()
.unwrap();
assert!(
result.status.success(),
"{}",
String::from_utf8_lossy(&result.stderr)
);
assert_eq!(
String::from_utf8(result.stdout).unwrap(),
format!("{arguments:?}\n")
);
}
let result = Command::new(binary)
.arg(OsString::from_vec(vec![0xff, 0xfe]))
.env_clear()
.output()
.unwrap();
assert!(!result.status.success());
assert!(result.stdout.is_empty());
let error = String::from_utf8(result.stderr).unwrap();
assert!(
error.contains("ArgumentsResult.Failed") && error.contains("lib/dever/process.dever:"),
"{error}"
);
}
@@ -0,0 +1,108 @@
//! 真实 Dever command Port 经过检查、Rust 生成与原生执行,删除源码后仍独立运行。
use super::{BuildTarget, Fixture, Project, Resource, app, build, command, write};
use std::{fs, process::Command};
#[test]
fn native_command_preserves_binary_streams_argv_and_nonzero_exit() {
let fixture = Fixture::new(&[]);
let root = &fixture.directory.0;
let body = "output = port.execute([\"--echo\", \"a b\", \"$(touch never)\", \"雪☃\"], dever.bytes.fromInts([0, 255, 1]))\n answer = output.code == 7 and output.stdout == dever.bytes.fromInts([0, 255, 1]) and output.stderr == dever.bytes.fromText(\"a b\\n$(touch never)\\n雪☃\") and input == 8";
write(
root,
"module/demo/value/app.dever",
app("0")
.replace("answer: Int", "answer: Bool")
.replace("answer = 0", body),
);
write(root, "module/demo/value/api.dever", command());
write(
root,
"module/demo/value/port.dever",
"execute(args: List<Text>, stdin: Bytes?) (output: dever.process.Output) fails dever.process.Error\n",
);
write(
root,
"module/demo/value/adapter.dever",
"external command \"command\" {}\n",
);
let project = Project::load(root).unwrap();
let program = project.check().unwrap();
let external = program.external_workers().next().unwrap();
let fixture = root.join("fixture");
let source = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../test/rust/runtime/worker/fixture.rs");
let compiled = Command::new("rustup")
.args(["run", "1.95.0", "rustc", "--edition=2024", "-O"])
.arg(source)
.arg("-o")
.arg(&fixture)
.output()
.unwrap();
assert!(
compiled.status.success(),
"{}",
String::from_utf8_lossy(&compiled.stderr)
);
let launch = serde_json::to_vec(&serde_json::json!({"format":"dever-command-launch-v1","ecosystem":"command","entry":external.entry,"executable":external.entry,"arguments":[],"working_directory":"module/demo/value"})).unwrap();
let resources = [
(external.entry.clone(), fs::read(&fixture).unwrap(), true),
(
format!("{}.dever-command.json", external.entry),
launch,
false,
),
]
.into_iter()
.map(|(path, bytes, executable)| Resource {
path,
bytes,
executable,
})
.collect::<Vec<_>>();
let binary = build::build_with_resources(
&program,
&project.sources,
"main",
&root.join("app"),
BuildTarget::host().unwrap(),
&resources,
)
.unwrap();
fs::remove_dir_all(root.join("module")).unwrap();
fs::remove_file(fixture).unwrap();
for _ in 0..2 {
let result = Command::new(&binary)
.env_clear()
.args(["demo.value.value", "{\"input\":8}"])
.output()
.unwrap();
assert!(
result.status.success(),
"{}",
String::from_utf8_lossy(&result.stderr)
);
assert_eq!(
result.stdout,
b"{\"code\":0,\"message\":\"ok\",\"data\":true}\n"
);
}
write(
root,
"config/setting.json",
r#"{"adapter":{"demo.value":{"command":{"output_limit":1}}}}"#,
);
let failed = Command::new(&binary)
.env_clear()
.args(["demo.value.value", "{\"input\":8}"])
.output()
.unwrap();
assert!(!failed.status.success());
let error = String::from_utf8_lossy(&failed.stderr);
assert!(
error.contains("external command failed") && error.contains("adapter.dever:"),
"{error}"
);
let output = serde_json::from_slice::<serde_json::Value>(&failed.stdout);
assert!(output.is_err() || output.unwrap()["code"] != 0);
}
+132
View File
@@ -0,0 +1,132 @@
//! 在同一 Worker 原生产物上验证持久 Job 的 worker-only 服务根。
use super::{roots::Server, write};
use serde_json::{Value, json};
use std::{
fs,
path::Path,
process::{Command, Stdio},
time::{Duration, Instant},
};
pub(super) fn prepare(root: &Path) {
let app = root.join("module/demo/value/app.dever");
let source = fs::read_to_string(&app).unwrap();
fs::write(app, format!("{source}{APP}")).unwrap();
let api = root.join("module/demo/value/api.dever");
let source = fs::read_to_string(&api).unwrap();
fs::write(api, format!("{source}\ncmd submit = app.submit\n")).unwrap();
write(root, "module/demo/value/job.dever", JOB);
}
pub(super) fn run(binary: &Path, root: &Path) {
let config = root.join("setting-worker.json");
fs::write(
&config,
json!({
"database":{"default":{"type":"sqlite","path":"data/worker.db","max_connections":4}},
"sites":{"public":{"path":"","hosts":["fixture.test"]}},
"http":{"host":"worker-only-no-listener","port":0},
"runtime":{"mode":"worker","shutdown_ms":1000},
"job":{"workers":1,"poll_ms":10,"lease_ms":2000}
})
.to_string(),
)
.unwrap();
let database = root.join("data/worker.db");
for input in [8, -4] {
let submitted = Command::new(binary)
.env_clear()
.arg("--config")
.arg(&config)
.args([
"demo.value.submit",
&json!({"input":input,"key":format!("root-{input}")}).to_string(),
])
.output()
.unwrap();
assert!(
submitted.status.success(),
"{}",
String::from_utf8_lossy(&submitted.stderr)
);
let response: Value = serde_json::from_slice(&submitted.stdout).unwrap();
let id = response["data"].as_str().unwrap();
let pending = queue_row(&database, id);
assert_eq!(pending["state"], "pending");
assert_eq!(pending["attempt"], 0);
assert_eq!(pending["execution"], json!({"kind":"System","tenant":null}));
assert_eq!(pending["tenant"], 0);
let log = root.join("job.stderr");
let mut server = Server(
Command::new(binary)
.env_clear()
.arg("--config")
.arg(&config)
.stdout(Stdio::null())
.stderr(fs::File::create(&log).unwrap())
.spawn()
.unwrap(),
);
let deadline = Instant::now() + Duration::from_secs(10);
let completed = loop {
assert!(
server.0.try_wait().unwrap().is_none(),
"worker root exited: {}",
fs::read_to_string(&log).unwrap()
);
let row = queue_row(&database, id);
if row["state"] == "completed" {
break row;
}
assert!(
row["state"] == "pending" || row["state"] == "running",
"{row}"
);
assert!(Instant::now() < deadline, "Job did not complete: {row}");
std::thread::sleep(Duration::from_millis(20));
};
assert_eq!(completed["attempt"], 1);
assert_eq!(completed["execution"], pending["execution"]);
assert_eq!(completed["rows"], 0, "readonly Job changed Model data");
server.shutdown(&log, input == -4);
assert_eq!(queue_row(&database, id), completed);
}
}
fn queue_row(database: &Path, id: &str) -> Value {
// 使用 dev 已有 sqlite3 模块只读检查本测试队列;不向私有队列表注入数据。
let output = Command::new("python3")
.args(["-c", r#"import json, pathlib, sqlite3, sys
with sqlite3.connect(pathlib.Path(sys.argv[1]).as_uri() + '?mode=ro', uri=True) as database:
row = database.execute('SELECT state,attempt,execution,execution_tenant FROM _dever_job WHERE id=?', (sys.argv[2],)).fetchone()
rows = database.execute('SELECT COUNT(*) FROM demo_value_model').fetchone()[0]
print(json.dumps(dict(state=row[0], attempt=row[1], execution=json.loads(row[2]), tenant=row[3], rows=rows)))
"#])
.arg(database)
.arg(id)
.output()
.unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
serde_json::from_slice(&output.stdout).unwrap()
}
const APP: &str = r#"type Work { input: Int }
submit(input: Int, key: Text) (id: Id) {
id = dever.job.enqueue(job.perform, Work { input = input }, key)
}
perform(input: Work) () { answer = stored(input.input) }
"#;
const JOB: &str = r#"database default
long job perform(input: app.Work) () retry(1) timeout(1000) {
app.perform(input)
}
"#;
+158
View File
@@ -0,0 +1,158 @@
//! 同一原生产物覆盖普通 CMD、只读数据库 CMD 和服务根的 Worker 收口。
use super::{app, command, command_cases, write};
use std::{
fs,
io::{Read, Write},
net::{TcpListener, TcpStream},
path::Path,
process::{Child, Command, Stdio},
time::{Duration, Instant},
};
pub(super) fn prepare(root: &Path) {
let mut source = app("port.native(input) + input");
source.push_str(
r#"stored(input: Int) (answer: Int) {
count = model.count()
answer = count + port.native(input) + input
}
"#,
);
write(root, "module/demo/value/app.dever", source);
let mut source = command();
source.push_str(
r#"cmd stored = app.stored
public get stored = app.stored
"#,
);
write(root, "module/demo/value/api.dever", source);
write(
root,
"module/demo/value/model.dever",
r#"global type Value { label: Text }
"#,
);
write(
root,
"config/setting.json",
r#"{"database":{"default":{"type":"sqlite","path":"data/worker.db","max_connections":1}}}"#,
);
}
pub(super) fn commands(binary: &Path) {
for entry in ["demo.value.value", "demo.value.stored"] {
command_cases(binary, entry);
}
assert!(binary.parent().unwrap().join("data/worker.db").is_file());
}
pub(super) fn http(binary: &Path, root: &Path) {
for bad_shutdown in [false, true] {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
drop(listener);
let config = root.join("setting-http.json");
fs::write(
&config,
serde_json::to_vec(&serde_json::json!({
"http":{"host":"127.0.0.1","port":port},
"sites":{"public":{"path":"","hosts":["fixture.test"]}},
"runtime":{"mode":"api","shutdown_ms":1000},
"database":{"default":{"type":"sqlite","path":"data/worker.db","max_connections":1}}
}))
.unwrap(),
)
.unwrap();
let log = root.join("http.stderr");
let mut server = Server(
Command::new(binary)
.env_clear()
.arg("--config")
.arg(config)
.stdout(Stdio::null())
.stderr(fs::File::create(&log).unwrap())
.spawn()
.unwrap(),
);
let deadline = Instant::now() + Duration::from_secs(5);
while TcpStream::connect(("127.0.0.1", port)).is_err() {
assert!(
server.0.try_wait().unwrap().is_none(),
"server exited during startup: {}",
fs::read_to_string(&log).unwrap()
);
assert!(Instant::now() < deadline, "server did not bind");
std::thread::sleep(Duration::from_millis(10));
}
assert_eq!(request(port, 8), (200, 50));
let (status, identity) = request(port, 99);
assert_eq!(status, 200);
assert!(identity > 99, "Worker PID was not returned");
assert_eq!(request(port, -3).0, 400);
if bad_shutdown {
// 当前 HTTP 调用先成功;停机错误只能在共享服务根结束时传播。
assert_eq!(request(port, -4), (200, 38));
}
for _ in 0..2 {
assert_eq!(request(port, 99), (200, identity));
}
server.shutdown(&log, bad_shutdown);
}
}
fn request(port: u16, input: i64) -> (u16, i64) {
let mut socket = TcpStream::connect(("127.0.0.1", port)).unwrap();
socket
.set_read_timeout(Some(Duration::from_secs(3)))
.unwrap();
write!(socket, "GET /demo/value/stored?input={input} HTTP/1.1\r\nHost: fixture.test\r\nConnection: close\r\n\r\n").unwrap();
let mut response = String::new();
socket.read_to_string(&mut response).unwrap();
let (head, body) = response.split_once("\r\n\r\n").unwrap();
let status = head.split_whitespace().nth(1).unwrap().parse().unwrap();
let body: serde_json::Value = serde_json::from_str(body).unwrap();
if status == 200 {
assert_eq!(body["code"], 0);
(status, body["data"].as_i64().unwrap())
} else {
assert_eq!(body["code"], status);
assert!(body["data"].is_null());
(status, 0)
}
}
pub(super) struct Server(pub(super) Child);
impl Server {
pub(super) fn shutdown(&mut self, log: &Path, bad_shutdown: bool) {
rustix::process::kill_process(
rustix::process::Pid::from_raw(self.0.id() as i32).unwrap(),
rustix::process::Signal::TERM,
)
.unwrap();
let deadline = Instant::now() + Duration::from_secs(5);
let status = loop {
if let Some(status) = self.0.try_wait().unwrap() {
break status;
}
assert!(Instant::now() < deadline, "service root did not finish");
std::thread::sleep(Duration::from_millis(10));
};
let error = fs::read_to_string(log).unwrap();
assert_eq!(status.success(), !bad_shutdown, "{error}");
assert_eq!(
error.contains("invalid Worker shutdown acknowledgement"),
bad_shutdown,
"{error}"
);
}
}
impl Drop for Server {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}