feat: publish Dever core with project scaffolding and versioned AI skills
This commit is contained in:
1 parent
70fa73cdc2
commit
ac458ffa7f
960 files changed
+221319
-1163
No files matched your search
@@ -0,0 +1,337 @@
|
||||
#[path = "support/temp.rs"]
|
||||
mod temp;
|
||||
|
||||
use bytes::Bytes;
|
||||
use dever_runtime::api::upload;
|
||||
use dever_runtime::config::UploadLimits;
|
||||
use http_body_util::StreamBody;
|
||||
use hyper::body::Frame;
|
||||
use std::convert::Infallible;
|
||||
|
||||
fn body(
|
||||
bytes: &[u8],
|
||||
chunk: usize,
|
||||
) -> impl hyper::body::Body<Data = Bytes, Error = Infallible> + Unpin {
|
||||
StreamBody::new(futures_util::stream::iter(
|
||||
bytes
|
||||
.chunks(chunk)
|
||||
.map(|chunk| Ok(Frame::data(Bytes::copy_from_slice(chunk))))
|
||||
.collect::<Vec<Result<Frame<Bytes>, Infallible>>>(),
|
||||
))
|
||||
}
|
||||
|
||||
fn multipart(filename: &str, bytes: &[u8]) -> Vec<u8> {
|
||||
let mut body = format!("--boundary\r\nContent-Disposition: form-data; name=\"caption\"\r\n\r\nhello\r\n--boundary\r\nContent-Disposition: form-data; name=\"file\"; filename=\"{filename}\"\r\nContent-Type: image/png\r\n\r\n").into_bytes();
|
||||
body.extend_from_slice(bytes);
|
||||
body.extend_from_slice(b"\r\n--boundary--\r\n");
|
||||
body
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn streaming_split_boundaries_preserve_bytes_and_remove_owned_files() {
|
||||
let directory = temp::TemporaryDirectory::new();
|
||||
let mut payload = vec![b'x'; 170_001];
|
||||
payload.extend_from_slice(b"\r\n--boundaryX is file content\r\n--boundary--X\r\n--boundary-");
|
||||
for chunk in [1, 7, 16_384, 250_000] {
|
||||
let bytes = multipart("photo.png", &payload);
|
||||
let mut parts = upload::decode(
|
||||
body(&bytes, chunk),
|
||||
"multipart/form-data; boundary=boundary",
|
||||
directory.path(),
|
||||
UploadLimits::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
parts
|
||||
.json("caption", dever_runtime::api::QueryValue::Text)
|
||||
.unwrap(),
|
||||
"\"hello\""
|
||||
);
|
||||
let file = parts.take("file").unwrap();
|
||||
assert_eq!(file.filename(), "photo.png");
|
||||
assert_eq!(file.content_type(), "image/png");
|
||||
assert_eq!(file.size(), payload.len() as i64);
|
||||
let files = std::fs::read_dir(directory.path())
|
||||
.unwrap()
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.unwrap();
|
||||
assert_eq!(files.len(), 1);
|
||||
assert_eq!(std::fs::read(files[0].path()).unwrap(), payload);
|
||||
parts.finish().unwrap();
|
||||
file.close();
|
||||
assert_eq!(std::fs::read_dir(directory.path()).unwrap().count(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn malformed_limits_and_duplicate_fields_cleanup_partial_files() {
|
||||
let directory = temp::TemporaryDirectory::new();
|
||||
for bytes in [
|
||||
multipart("../escape", b"hello"),
|
||||
multipart("x", b"hello")[..150].to_vec(),
|
||||
multipart("x", b"hello").repeat(2),
|
||||
] {
|
||||
assert!(
|
||||
upload::decode(
|
||||
body(&bytes, 3),
|
||||
"multipart/form-data; boundary=boundary",
|
||||
directory.path(),
|
||||
UploadLimits::default()
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(std::fs::read_dir(directory.path()).unwrap().count(), 0);
|
||||
}
|
||||
let bytes = multipart("x", &[0; 40_000]);
|
||||
for limits in [
|
||||
UploadLimits {
|
||||
file_bytes: 100,
|
||||
..Default::default()
|
||||
},
|
||||
UploadLimits {
|
||||
total_bytes: 256,
|
||||
file_bytes: 100,
|
||||
..Default::default()
|
||||
},
|
||||
UploadLimits {
|
||||
header_bytes: 10,
|
||||
..Default::default()
|
||||
},
|
||||
] {
|
||||
assert!(
|
||||
upload::decode(
|
||||
body(&bytes, 1024),
|
||||
"multipart/form-data; boundary=boundary",
|
||||
directory.path(),
|
||||
limits
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(std::fs::read_dir(directory.path()).unwrap().count(), 0);
|
||||
}
|
||||
let duplicate = b"--boundary\r\nContent-Disposition: form-data; name=\"file\"; filename=\"first\"\r\n\r\nfirst\r\n--boundary\r\nContent-Disposition: form-data; name=\"file\"; filename=\"second\"\r\n\r\nsecond\r\n--boundary--\r\n";
|
||||
assert!(
|
||||
upload::decode(
|
||||
body(duplicate, 1),
|
||||
"multipart/form-data; boundary=boundary",
|
||||
directory.path(),
|
||||
UploadLimits::default()
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(std::fs::read_dir(directory.path()).unwrap().count(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cancellation_drops_partial_upload_owner() {
|
||||
let directory = temp::TemporaryDirectory::new();
|
||||
let bytes = multipart("x", &[0; 40_000]);
|
||||
let frames = futures_util::stream::iter(vec![Ok::<_, Infallible>(Frame::data(
|
||||
Bytes::copy_from_slice(&bytes[..30_000]),
|
||||
))]);
|
||||
let stalled = futures_util::StreamExt::chain(frames, futures_util::stream::pending());
|
||||
let mut decoding = Box::pin(upload::decode(
|
||||
StreamBody::new(stalled),
|
||||
"multipart/form-data; boundary=boundary",
|
||||
directory.path(),
|
||||
UploadLimits::default(),
|
||||
));
|
||||
let writing = async {
|
||||
for _ in 0..100 {
|
||||
if std::fs::read_dir(directory.path())
|
||||
.unwrap()
|
||||
.any(|file| file.unwrap().metadata().unwrap().len() > 1000)
|
||||
{
|
||||
return;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
}
|
||||
panic!("decoder did not spool the initial body frames");
|
||||
};
|
||||
tokio::select! {
|
||||
_ = writing => {},
|
||||
_ = &mut decoding => panic!("incomplete decoder unexpectedly finished"),
|
||||
}
|
||||
drop(decoding);
|
||||
for _ in 0..100 {
|
||||
if std::fs::read_dir(directory.path()).unwrap().count() == 0 {
|
||||
return;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||
}
|
||||
panic!("cancelled upload retained a temporary file");
|
||||
}
|
||||
|
||||
fn sources(app: &str, route: &str) -> dever_core::source::SourceMap {
|
||||
let mut sources = dever_core::source::SourceMap::default();
|
||||
sources.add("media/image/app.dever", app);
|
||||
sources.add("media/image/api.dever", route);
|
||||
sources
|
||||
}
|
||||
|
||||
fn storage_sources() -> dever_core::source::SourceMap {
|
||||
let mut sources = sources(
|
||||
"accept(file: Upload) (key: Uuid) { key = port.store(file) }",
|
||||
"post upload = app.accept",
|
||||
);
|
||||
sources.add(
|
||||
"media/image/port.dever",
|
||||
"store(file: Upload) (key: Uuid) fails dever.storage.PutResult",
|
||||
);
|
||||
sources.add(
|
||||
"media/image/adapter.dever",
|
||||
"port.store(file: Upload) (key: Uuid) { key = dever.storage.put(file) }",
|
||||
);
|
||||
sources
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upload_affinity_and_route_contract_are_checked() {
|
||||
for (app, route) in [
|
||||
(
|
||||
"accept(file: Upload) (size: Int) { size = dever.api.upload_size(file) }",
|
||||
"post upload = app.accept",
|
||||
),
|
||||
(
|
||||
"accept(file: Upload) (size: Int) { dever.api.close_upload(file)\nsize = dever.api.upload_size(file) }",
|
||||
"post upload = app.accept",
|
||||
),
|
||||
(
|
||||
"accept(file: Upload) (size: Int) { size = dever.api.upload_size(file)\ndever.api.close_upload(file) }",
|
||||
"get upload = app.accept",
|
||||
),
|
||||
(
|
||||
"accept(file: Upload) (size: Int) { size = dever.api.upload_size(file)\ndever.api.close_upload(file) }",
|
||||
"cmd upload = app.accept",
|
||||
),
|
||||
(
|
||||
"type Payload { file: Upload }\naccept(file: Payload) (size: Int) { size = 1 }",
|
||||
"post upload = app.accept",
|
||||
),
|
||||
(
|
||||
"accept(file: Upload) (key: Uuid) { key = dever.storage.put(file) }",
|
||||
"post upload = app.accept",
|
||||
),
|
||||
(
|
||||
"accept(file: Upload) (size: Int) { size = helper(file) }\nhelper(file: Upload) (size: Int) { size = dever.api.upload_size(file)\ndever.api.close_upload(file) }",
|
||||
"post upload = app.accept",
|
||||
),
|
||||
] {
|
||||
assert!(
|
||||
dever_core::check(&sources(app, route)).is_err(),
|
||||
"unexpected accepted: {app}"
|
||||
);
|
||||
}
|
||||
let sources = sources(
|
||||
"accept(file: Upload, caption: Text) (size: Int) { size = dever.api.upload_size(file)\nname = dever.api.upload_filename(file)\ndever.api.close_upload(file) }",
|
||||
"post upload = app.accept",
|
||||
);
|
||||
dever_core::check(&sources).unwrap_or_else(|errors| {
|
||||
panic!(
|
||||
"{}",
|
||||
errors
|
||||
.iter()
|
||||
.map(|error| error.render(&sources))
|
||||
.collect::<String>()
|
||||
)
|
||||
});
|
||||
let sources = storage_sources();
|
||||
dever_core::check(&sources).unwrap_or_else(|errors| {
|
||||
panic!(
|
||||
"{}",
|
||||
errors
|
||||
.iter()
|
||||
.map(|error| error.render(&sources))
|
||||
.collect::<String>()
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upload_route_native_compiles_with_owned_resource() {
|
||||
use std::io::{Read, Write};
|
||||
let sources = storage_sources();
|
||||
let program = dever_core::check(&sources).unwrap_or_else(|errors| {
|
||||
panic!(
|
||||
"{}",
|
||||
errors
|
||||
.iter()
|
||||
.map(|error| error.render(&sources))
|
||||
.collect::<String>()
|
||||
)
|
||||
});
|
||||
let native = dever_core::native::compile_project(
|
||||
&program,
|
||||
&sources,
|
||||
std::ffi::OsStr::new("/root/.cargo/bin/rustc"),
|
||||
dever_runtime::config::RuntimeProfile::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(native.executable().is_file());
|
||||
let directory = temp::TemporaryDirectory::new();
|
||||
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
drop(listener);
|
||||
std::fs::create_dir(directory.path().join("config")).unwrap();
|
||||
std::fs::write(
|
||||
directory.path().join("config/setting.json"),
|
||||
format!(r#"{{"http":{{"host":"127.0.0.1","port":{port},"timeout_ms":1000}}}}"#),
|
||||
)
|
||||
.unwrap();
|
||||
let executable = directory.path().join("upload-test");
|
||||
native.save(&executable).unwrap();
|
||||
let mut child = Child(
|
||||
std::process::Command::new(executable)
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap(),
|
||||
);
|
||||
let started = std::time::Instant::now();
|
||||
let mut peer = loop {
|
||||
assert!(
|
||||
started.elapsed() < std::time::Duration::from_secs(5),
|
||||
"upload listener startup timeout"
|
||||
);
|
||||
if let Some(status) = child.0.try_wait().unwrap() {
|
||||
panic!("upload application exited: {status}");
|
||||
}
|
||||
match std::net::TcpStream::connect(("127.0.0.1", port)) {
|
||||
Ok(peer) => break peer,
|
||||
Err(_) => std::thread::sleep(std::time::Duration::from_millis(10)),
|
||||
}
|
||||
};
|
||||
peer.set_read_timeout(Some(std::time::Duration::from_secs(3)))
|
||||
.unwrap();
|
||||
let bytes = b"--boundary\r\nContent-Disposition: form-data; name=\"file\"; filename=\"hello.txt\"\r\n\r\nhello\r\n--boundary--\r\n";
|
||||
write!(peer, "POST /media/image/upload HTTP/1.1\r\nHost: localhost\r\nContent-Type: multipart/form-data; boundary=boundary\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", bytes.len()).unwrap();
|
||||
peer.write_all(bytes).unwrap();
|
||||
let mut response = String::new();
|
||||
peer.read_to_string(&mut response).unwrap();
|
||||
assert!(response.starts_with("HTTP/1.1 200"), "{response}");
|
||||
let envelope: serde_json::Value =
|
||||
serde_json::from_str(response.split_once("\r\n\r\n").unwrap().1).unwrap();
|
||||
let key = envelope["data"].as_str().unwrap();
|
||||
assert!(dever_runtime::orm::Uuid::parse(key).is_ok());
|
||||
assert_eq!(
|
||||
std::fs::read(directory.path().join("data/upload").join(key)).unwrap(),
|
||||
b"hello"
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read_dir(directory.path().join("data/tmp"))
|
||||
.unwrap()
|
||||
.count(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
struct Child(std::process::Child);
|
||||
impl Drop for Child {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.0.kill();
|
||||
let _ = self.0.wait();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user