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

+424
View File
@@ -0,0 +1,424 @@
use super::*;
#[path = "http/choice.rs"]
mod choice;
use std::{
io::{Read, Write},
net::{TcpListener, TcpStream},
time::{Duration, Instant},
};
pub(super) struct Server(pub(super) std::process::Child);
impl Drop for Server {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
pub(super) fn request(
port: u16,
method: &str,
path: &str,
host: &str,
headers: &str,
body: &str,
) -> (u16, String) {
let (status, _, body) = request_head(port, method, path, host, headers, body);
(status, body)
}
pub(super) fn request_head(
port: u16,
method: &str,
path: &str,
host: &str,
headers: &str,
body: &str,
) -> (u16, String, String) {
let mut socket = TcpStream::connect(("127.0.0.1", port)).unwrap();
socket
.set_read_timeout(Some(Duration::from_secs(3)))
.unwrap();
write!(socket, "{method} {path} HTTP/1.1\r\nHost: {host}\r\nConnection: close\r\nContent-Length: {}\r\n{headers}\r\n{body}", body.len()).unwrap();
let mut response = String::new();
socket.read_to_string(&mut response).unwrap();
let (head, body) = response.split_once("\r\n\r\n").unwrap();
(
head.split_whitespace().nth(1).unwrap().parse().unwrap(),
head.into(),
body.into(),
)
}
#[test]
fn json_cmd_api_budget_keeps_input_boundaries_and_raw_spelling() {
let directory = Directory::new();
let root = directory.0.join("module/demo/input");
std::fs::create_dir_all(&root).unwrap();
std::fs::write(
root.join("app.dever"),
r#"type Packet { payload: Json
note: Text?
count: Int
extra: Json? }
accept(value: Packet) (answer: Int) { answer = value.count }
echo(value: Json) (answer: Json) { answer = value }
"#,
)
.unwrap();
let mut api = "".to_owned();
for (method, visibility) in [("cmd", ""), ("post", "public ")] {
for name in ["accept", "echo"] {
api.push_str(&format!("{visibility}{method} {name} = app.{name}\n"));
}
}
std::fs::write(root.join("api.dever"), api).unwrap();
let project = Project::load(&directory.0).unwrap();
let program = project.check().unwrap();
let binary =
build::build(&program, &project.sources, "main", &directory.0.join("app")).unwrap();
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
drop(listener);
let config = directory.0.join("setting.json");
std::fs::write(&config, format!("{{\"http\":{{\"host\":\"127.0.0.1\",\"port\":{port}}},\"sites\":{{\"public\":{{\"path\":\"\",\"hosts\":[\"fixture.test\"]}}}}}}" )).unwrap();
let raw = r#"{ "values": [1e9999,-0,1.00e+2,"\u96ea"] }"#;
let input = format!("{{\"value\":{raw}}}");
let expected = format!("{{\"code\":0,\"message\":\"ok\",\"data\":{raw}}}");
let command = Command::new(&binary)
.args([
"--config",
config.to_str().unwrap(),
"demo.input.echo",
&input,
])
.output()
.unwrap();
assert!(command.status.success(), "{:?}", command.stderr);
assert_eq!(
String::from_utf8(command.stdout).unwrap(),
format!("{expected}\n")
);
let mut server = Server(
Command::new(&binary)
.args(["--config", config.to_str().unwrap()])
.stdout(std::process::Stdio::null())
.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());
assert!(Instant::now() < deadline);
std::thread::sleep(Duration::from_millis(10));
}
let call = |name: &str, input: &str| {
request(
port,
"POST",
&format!("/demo/input/{name}"),
"fixture.test",
"Content-Type: application/json\r\n",
input,
)
};
assert_eq!(call("echo", &input), (200, expected));
for input in [
r#"[{"payload":null,"count":0}]"#,
r#"{"value":[null,null,0,null]}"#,
r#"{"value":{"payload":null,"note":null,"note":null,"count":0}}"#,
r#"{"value":{"payload":{"a":0,"\u0061":1},"count":0}}"#,
r#"{"value":{"payload":"\ud800","count":0}}"#,
r#"{"value":{"payload":null,"count":0,"unknown":0}}"#,
r#"{"value":{"payload":null,"count":0,"extra":null,"extra":null}}"#,
r#"{"value":{"payload":null,"count":0}} {}"#,
] {
assert_eq!(call("accept", input).0, 400, "{input}");
let command = Command::new(&binary)
.args([
"--config",
config.to_str().unwrap(),
"demo.input.accept",
input,
])
.output()
.unwrap();
assert!(!command.status.success());
assert!(String::from_utf8_lossy(&command.stderr).contains("invalid command input"));
}
// 总值数边界的正文超过 Linux 单个 argv 的限制,通过 HTTP 验证同一 codec。
let limit = dever_runtime::wire::document::MAX_VALUES;
for (count, extra) in [(limit - 4, ""), (limit - 6, ",\"extra\":[0]")] {
let payload = vec!["0"; count].join(",");
for (note, status) in [("", 200), (",\"note\":null", 400)] {
let input =
format!("{{\"value\":{{\"payload\":[{payload}],\"count\":0{extra}{note}}}}}");
assert_eq!(call("accept", &input).0, status);
}
}
for (depth, status) in [(62, 200), (63, 400)] {
let payload = "[".repeat(depth) + &"]".repeat(depth);
let input = format!("{{\"value\":{{\"payload\":{payload},\"count\":0}}}}");
assert_eq!(call("accept", &input).0, status);
}
}
#[test]
fn automatic_http_runs_dever_app_strict_inputs_sites_faults_cmd_and_shutdown() {
application(true);
application(false);
}
fn application(commands: bool) {
let directory = Directory::new();
let mut project = Project::load(&fixture().with_file_name("http-application")).unwrap();
if !commands {
let source = project
.sources
.iter_mut()
.find(|source| source.path.ends_with("greeting/api.dever"))
.unwrap();
*source = dever::syntax::source::Source::new(
source.id,
&source.path,
source.text.split("cmd ").next().unwrap().into(),
);
}
if commands {
let source = project
.sources
.iter_mut()
.find(|source| source.path.ends_with("greeting/api.dever"))
.unwrap();
*source = dever::syntax::source::Source::new(
source.id,
&source.path,
format!("{}\npublic post greet = app.greet\n", source.text),
);
}
let program = project.check().unwrap();
let binary =
build::build(&program, &project.sources, "main", &directory.0.join("app")).unwrap();
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let port = listener.local_addr().unwrap().port();
drop(listener);
let config = directory.0.join("setting.json");
std::fs::write(&config, format!("{{\"http\":{{\"host\":\"127.0.0.1\",\"port\":{port}}},\"sites\":{{\"public\":{{\"path\":\"\",\"hosts\":[\"fixture.test\"]}}}}}}" )).unwrap();
let fragment = serde_json::json!({"source": " \n{\"a\": [1e9999,-0]}\t "}).to_string();
let expected_fragment = "{\"code\":0,\"message\":\"ok\",\"data\":{\"value\":{\"a\": [1e9999,-0]},\"label\":\"雪\\\"\"}}";
if commands {
let command = Command::new(&binary)
.args([
"--config",
config.to_str().unwrap(),
"demo.greeting.greet",
"{\"name\":\"CMD\"}",
])
.output()
.unwrap();
assert!(
command.status.success(),
"{}",
String::from_utf8_lossy(&command.stderr)
);
assert!(String::from_utf8_lossy(&command.stdout).contains("Hello, CMD"));
let command = Command::new(&binary)
.args([
"--config",
config.to_str().unwrap(),
"demo.greeting.fragment",
&fragment,
])
.output()
.unwrap();
assert!(
command.status.success(),
"{}",
String::from_utf8_lossy(&command.stderr)
);
assert_eq!(
String::from_utf8(command.stdout).unwrap(),
format!("{expected_fragment}\n")
);
}
let mut server = Server(
Command::new(&binary)
.args(["--config", config.to_str().unwrap()])
.stdout(std::process::Stdio::null())
.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"
);
assert!(Instant::now() < deadline, "server did not bind");
std::thread::sleep(Duration::from_millis(10));
}
let (status, body) = request(
port,
"POST",
"/demo/greeting/fragment",
"fixture.test",
"Content-Type: application/json\r\n",
&fragment,
);
assert_eq!(status, 200, "{body}");
assert_eq!(body, expected_fragment);
let (status, head, body) =
request_head(port, "PUT", "/demo/greeting/greet", "fixture.test", "", "");
assert_eq!(status, 405);
assert_eq!(
body,
"{\"code\":405,\"message\":\"method not allowed\",\"data\":null}"
);
let allowed = head
.lines()
.filter_map(|line| line.split_once(':'))
.find(|(name, _)| name.eq_ignore_ascii_case("allow"))
.unwrap()
.1
.split(',')
.map(str::trim)
.collect::<std::collections::BTreeSet<_>>();
assert_eq!(
allowed,
if commands {
vec!["GET", "POST"]
} else {
vec!["GET"]
}
.into_iter()
.collect()
);
for (path, host) in [
("/demo/greeting/greet", "wrong.test"),
("/demo/greeting/absent", "fixture.test"),
] {
let (status, head, _) = request_head(port, "PUT", path, host, "", "");
assert_eq!(status, 404);
assert!(!head.to_ascii_lowercase().contains("\r\nallow:"));
}
for expected in [400, 401, 403, 404, 409, 429, 500] {
let (status, body) = request(
port,
"GET",
&format!("/demo/greeting/reject?status={expected}"),
"fixture.test",
"",
"",
);
assert_eq!(status, expected, "{body}");
let body: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(body["code"], expected);
assert!(body["data"].is_null());
assert!(!body.to_string().contains("Problem"));
}
let (status, body) = request(
port,
"GET",
"/demo/greeting/greet?name=%E9%9B%AA",
"fixture.test",
"Cookie: stale=ignored\r\n",
"",
);
assert_eq!(status, 200);
assert_eq!(
body,
"{\"code\":0,\"message\":\"ok\",\"data\":\"Hello, 雪\"}"
);
for path in [
"/demo/greeting/greet",
"/demo/greeting/greet?name=a&name=b",
"/demo/greeting/greet?name=a&%6Eame=b",
"/demo/greeting/greet?name=%ZZ",
"/demo/greeting/greet?name=%",
"/demo/greeting/greet?name=%FF",
"/demo/greeting/greet?name",
"/demo/greeting/greet?name=a&extra=b",
] {
assert_eq!(request(port, "GET", path, "fixture.test", "", "").0, 400);
}
assert_eq!(
request(
port,
"GET",
"/demo/greeting/greet?name=a",
"wrong.test",
"",
""
)
.0,
404
);
assert_eq!(
request(
port,
"GET",
"/demo/greeting/greet?name=a",
"fixture.test",
"Authorization: Bearer invalid\r\n",
""
)
.0,
401
);
let json = "Content-Type: application/json\r\n";
for (body, status) in [
("{\"value\":41}", 200),
("{\"value\":1,\"value\":2}", 400),
("{\"value\":1.2}", 400),
("{\"value\":1,\"extra\":2}", 400),
("[1]", 400),
("{\"value\":null}", 400),
("{\"value\":9223372036854775807}", 500),
] {
let (actual, response) = request(
port,
"POST",
"/demo/greeting/pair",
"fixture.test",
json,
body,
);
assert_eq!(actual, status, "{response}");
assert!(!response.contains("arithmetic fault"));
}
assert_eq!(
request(
port,
"POST",
"/demo/greeting/pair",
"fixture.test",
"",
"{\"value\":1}"
)
.0,
415
);
assert!(
Command::new("sh")
.args([
"-c",
"kill -TERM \"$1\"",
"dever-test",
&server.0.id().to_string()
])
.status()
.unwrap()
.success()
);
let deadline = Instant::now() + Duration::from_secs(5);
loop {
if let Some(status) = server.0.try_wait().unwrap() {
assert!(status.success());
break;
}
assert!(Instant::now() < deadline, "shutdown did not release server");
std::thread::sleep(Duration::from_millis(10));
}
}