Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,361 @@
|
||||
//! 使用自有 registry 和缓存验证完整命令;不访问生产 daemon 或真实 registry。
|
||||
|
||||
use super::fixture::digest;
|
||||
use super::{Fixture, app, command, write};
|
||||
use dever::compiler::project::{
|
||||
Project,
|
||||
dependency::{self, Registry, Store},
|
||||
};
|
||||
use std::cell::RefCell;
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::{Cursor, Write};
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct Services {
|
||||
indexes: BTreeMap<String, Vec<serde_json::Value>>,
|
||||
downloads: BTreeMap<String, Vec<u8>>,
|
||||
cache: RefCell<BTreeMap<String, Vec<u8>>>,
|
||||
requests: RefCell<Vec<String>>,
|
||||
}
|
||||
|
||||
impl Services {
|
||||
pub(super) fn release(&mut self, name: &str, version: &str, dependencies: &[(&str, &str)]) {
|
||||
let manifest = serde_json::to_vec(&serde_json::json!({
|
||||
"format":"dever-package-v1", "name":name, "version":version,
|
||||
"dependencies":dependencies.iter().copied().collect::<BTreeMap<_, _>>(), "lib":[]
|
||||
}))
|
||||
.unwrap();
|
||||
let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new()));
|
||||
for (path, bytes) in [
|
||||
("dever-package.json".into(), manifest),
|
||||
(
|
||||
format!("module/{name}/value/app.dever"),
|
||||
app("input + 1").into_bytes(),
|
||||
),
|
||||
(
|
||||
format!("module/{name}/assets/home.html"),
|
||||
b"<h1>binary \0 resource</h1>".to_vec(),
|
||||
),
|
||||
] {
|
||||
zip.start_file(path, zip::write::SimpleFileOptions::default())
|
||||
.unwrap();
|
||||
zip.write_all(&bytes).unwrap();
|
||||
}
|
||||
let bytes = zip.finish().unwrap().into_inner();
|
||||
self.indexes.entry(name.into()).or_default().push(
|
||||
serde_json::json!({ "version":version,"sha256":digest(&bytes),"bytes":bytes.len() }),
|
||||
);
|
||||
self.downloads
|
||||
.insert(format!("/v1/packages/{name}/{version}.zip"), bytes);
|
||||
}
|
||||
}
|
||||
|
||||
impl Registry for Services {
|
||||
fn get(&self, path: &str, limit: usize) -> Result<Vec<u8>, String> {
|
||||
self.requests.borrow_mut().push(path.into());
|
||||
let bytes = if let Some(name) = path
|
||||
.strip_prefix("/v1/packages/")
|
||||
.and_then(|path| path.strip_suffix("/index.json"))
|
||||
{
|
||||
serde_json::to_vec(&serde_json::json!({"format":"dever-package-index-v1","name":name,"versions":self.indexes.get(name).ok_or("missing test index")?})).unwrap()
|
||||
} else {
|
||||
self.downloads
|
||||
.get(path)
|
||||
.ok_or("missing test archive")?
|
||||
.clone()
|
||||
};
|
||||
assert!(bytes.len() <= limit);
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl Store for Services {
|
||||
fn get(&self, sha256: &str, _bytes: u64) -> Result<Option<Vec<u8>>, String> {
|
||||
Ok(self.cache.borrow().get(sha256).cloned())
|
||||
}
|
||||
fn put(&self, bytes: &[u8]) -> Result<(), String> {
|
||||
self.cache
|
||||
.borrow_mut()
|
||||
.insert(digest(bytes), bytes.to_vec());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn versions(root: &std::path::Path) -> Vec<(String, String)> {
|
||||
let lock: serde_json::Value =
|
||||
serde_json::from_slice(&std::fs::read(root.join("dever.lock")).unwrap()).unwrap();
|
||||
lock["packages"].as_array().map_or(vec![], |packages| {
|
||||
packages
|
||||
.iter()
|
||||
.map(|package| {
|
||||
(
|
||||
package["name"].as_str().unwrap().into(),
|
||||
package["version"].as_str().unwrap().into(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_backtracks_then_update_and_remove_keep_a_valid_offline_closure() {
|
||||
let fixture = Fixture::new(&[]);
|
||||
let root = &fixture.directory.0;
|
||||
let mut services = Services::default();
|
||||
services.release("alpha", "1.0.0", &[("common_dep", "^1")]);
|
||||
services.release("alpha", "2.0.0", &[("common_dep", "^2")]);
|
||||
services.release("beta", "1.0.0", &[("common_dep", "^1")]);
|
||||
services.release("common_dep", "1.0.0", &[]);
|
||||
services.release("common_dep", "2.0.0", &[]);
|
||||
dependency::execute_with(
|
||||
"add",
|
||||
root,
|
||||
&["alpha@*".into(), "beta@^1".into()],
|
||||
&services,
|
||||
&services,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
versions(root),
|
||||
vec![
|
||||
("alpha".into(), "1.0.0".into()),
|
||||
("beta".into(), "1.0.0".into()),
|
||||
("common_dep".into(), "1.0.0".into())
|
||||
]
|
||||
);
|
||||
services.requests.borrow_mut().clear();
|
||||
dependency::execute_with("remove", root, &["beta".into()], &services, &services).unwrap();
|
||||
assert!(services.requests.borrow().is_empty());
|
||||
assert_eq!(versions(root)[0].1, "1.0.0");
|
||||
dependency::execute_with("update", root, &[], &services, &services).unwrap();
|
||||
assert_eq!(versions(root)[0].1, "2.0.0");
|
||||
dependency::execute_with("doctor", root, &[], &services, &services).unwrap();
|
||||
write(
|
||||
root,
|
||||
"module/demo/value/app.dever",
|
||||
app("alpha.value.value(common_dep.value.value(input))"),
|
||||
);
|
||||
write(root, "module/demo/value/api.dever", command());
|
||||
Project::load_with_artifacts(root, |sha256, bytes| {
|
||||
Store::get(&services, sha256, bytes).map(Option::unwrap)
|
||||
})
|
||||
.unwrap()
|
||||
.check()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restore_uses_exact_archives_without_resolving_or_rewriting_and_export_preserves_bytes() {
|
||||
let fixture = Fixture::new(&[]);
|
||||
let root = &fixture.directory.0;
|
||||
let mut services = Services::default();
|
||||
services.release("alpha", "1.0.0", &[]);
|
||||
dependency::execute_with("add", root, &["alpha@^1".into()], &services, &services).unwrap();
|
||||
let lock = std::fs::read(root.join("dever.lock")).unwrap();
|
||||
let setting = std::fs::read(root.join("config/setting.json")).unwrap();
|
||||
services.cache.borrow_mut().clear();
|
||||
services.requests.borrow_mut().clear();
|
||||
services.indexes.clear();
|
||||
dependency::install_with(root, &services, &services).unwrap();
|
||||
assert_eq!(
|
||||
&*services.requests.borrow(),
|
||||
&["/v1/packages/alpha/1.0.0.zip"]
|
||||
);
|
||||
assert_eq!(std::fs::read(root.join("dever.lock")).unwrap(), lock);
|
||||
assert_eq!(
|
||||
std::fs::read(root.join("config/setting.json")).unwrap(),
|
||||
setting
|
||||
);
|
||||
services.requests.borrow_mut().clear();
|
||||
let output = root.join("export");
|
||||
let report = dependency::export_with(root, "alpha", "assets", &output, &services).unwrap();
|
||||
let report: serde_json::Value = serde_json::from_str(&report).unwrap();
|
||||
let bytes = std::fs::read(output.join("home.html")).unwrap();
|
||||
assert_eq!(bytes, b"<h1>binary \0 resource</h1>");
|
||||
assert_eq!(report["files"][0]["sha256"], digest(&bytes));
|
||||
assert!(
|
||||
dependency::export_with(root, "alpha", "assets", &output, &services)
|
||||
.unwrap_err()
|
||||
.contains("already exists")
|
||||
);
|
||||
assert!(services.requests.borrow().is_empty());
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
assert_eq!(
|
||||
std::fs::metadata(&output).unwrap().permissions().mode() & 0o777,
|
||||
0o700
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::metadata(output.join("home.html"))
|
||||
.unwrap()
|
||||
.permissions()
|
||||
.mode()
|
||||
& 0o777,
|
||||
0o600
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolution_integrity_and_collision_errors_preserve_both_project_inputs() {
|
||||
for failure in ["conflict", "corrupt", "cycle", "collision"] {
|
||||
let fixture = Fixture::new(&[]);
|
||||
let root = &fixture.directory.0;
|
||||
let mut services = Services::default();
|
||||
services.release("alpha", "1.0.0", &[]);
|
||||
dependency::execute_with("add", root, &["alpha@^1".into()], &services, &services).unwrap();
|
||||
let setting = std::fs::read(root.join("config/setting.json")).unwrap();
|
||||
let lock = std::fs::read(root.join("dever.lock")).unwrap();
|
||||
services.release(
|
||||
"beta",
|
||||
"1.0.0",
|
||||
if failure == "cycle" {
|
||||
&[("beta", "^1")]
|
||||
} else if failure == "conflict" {
|
||||
&[("alpha", "^2")]
|
||||
} else {
|
||||
&[]
|
||||
},
|
||||
);
|
||||
if failure == "corrupt" {
|
||||
services
|
||||
.downloads
|
||||
.get_mut("/v1/packages/beta/1.0.0.zip")
|
||||
.unwrap()[0] ^= 1;
|
||||
}
|
||||
if failure == "collision" {
|
||||
write(root, "module/beta/private.txt", "user content");
|
||||
}
|
||||
assert!(
|
||||
dependency::execute_with("add", root, &["beta@^1".into()], &services, &services)
|
||||
.is_err(),
|
||||
"accepted {failure}"
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read(root.join("config/setting.json")).unwrap(),
|
||||
setting
|
||||
);
|
||||
assert_eq!(std::fs::read(root.join("dever.lock")).unwrap(), lock);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrupt_cache_is_an_error_not_a_download_and_export_rejects_escaping_paths() {
|
||||
let fixture = Fixture::new(&[]);
|
||||
let root = &fixture.directory.0;
|
||||
let mut services = Services::default();
|
||||
services.release("alpha", "1.0.0", &[]);
|
||||
dependency::execute_with("add", root, &["alpha@^1".into()], &services, &services).unwrap();
|
||||
for relative in [
|
||||
"../assets",
|
||||
"/assets",
|
||||
"assets//icons",
|
||||
"assets/.",
|
||||
"assets\\x",
|
||||
] {
|
||||
assert!(
|
||||
dependency::export_with(root, "alpha", relative, &root.join("out"), &services).is_err()
|
||||
);
|
||||
}
|
||||
services.requests.borrow_mut().clear();
|
||||
services.cache.borrow_mut().values_mut().next().unwrap()[0] ^= 1;
|
||||
assert!(
|
||||
dependency::install_with(root, &services, &services)
|
||||
.unwrap_err()
|
||||
.contains("SHA-256")
|
||||
);
|
||||
assert!(services.requests.borrow().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_transport_rejects_redirect_and_oversize_without_following() {
|
||||
use std::io::Read;
|
||||
for (reply, limit, success) in [
|
||||
(
|
||||
"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\n{}",
|
||||
2,
|
||||
true,
|
||||
),
|
||||
(
|
||||
"HTTP/1.1 200 OK\r\nContent-Length: 3\r\nConnection: close\r\n\r\n123",
|
||||
2,
|
||||
false,
|
||||
),
|
||||
(
|
||||
"HTTP/1.1 302 Found\r\nLocation: http://127.0.0.1:1/forbidden\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
|
||||
8,
|
||||
false,
|
||||
),
|
||||
] {
|
||||
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let origin = format!("http://{}", listener.local_addr().unwrap());
|
||||
let worker = std::thread::spawn(move || {
|
||||
let (mut peer, _) = listener.accept().unwrap();
|
||||
peer.set_read_timeout(Some(std::time::Duration::from_secs(5)))
|
||||
.unwrap();
|
||||
let mut received = Vec::new();
|
||||
let mut byte = [0];
|
||||
while !received.ends_with(b"\r\n\r\n") {
|
||||
peer.read_exact(&mut byte).unwrap();
|
||||
received.push(byte[0]);
|
||||
assert!(received.len() < 4096);
|
||||
}
|
||||
assert!(received.starts_with(b"GET /v1/packages/alpha/index.json HTTP/1.1\r\n"));
|
||||
peer.write_all(reply.as_bytes()).unwrap();
|
||||
});
|
||||
let registry = dependency::HttpRegistry::loopback(&origin).unwrap();
|
||||
assert_eq!(
|
||||
registry.get("/v1/packages/alpha/index.json", limit).is_ok(),
|
||||
success
|
||||
);
|
||||
worker.join().unwrap();
|
||||
}
|
||||
assert!(dependency::HttpRegistry::new("http://127.0.0.1:1").is_err());
|
||||
assert!(dependency::HttpRegistry::new("https://example.test/root").is_err());
|
||||
assert!(dependency::HttpRegistry::new("https://secret@example.test").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mutation_guard_rejects_busy_and_symlink_locks_without_changing_settings() {
|
||||
let fixture = Fixture::new(&[]);
|
||||
let root = &fixture.directory.0;
|
||||
let before = std::fs::read(root.join("config/setting.json")).unwrap();
|
||||
let path = root.join(".dever-project.lock");
|
||||
let lock = std::fs::File::create(&path).unwrap();
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).unwrap();
|
||||
lock.lock().unwrap();
|
||||
let services = Services::default();
|
||||
let error = dependency::execute_with("add", root, &["alpha@^1".into()], &services, &services)
|
||||
.unwrap_err();
|
||||
assert!(error.contains("busy"), "{error}");
|
||||
lock.unlock().unwrap();
|
||||
drop(lock);
|
||||
std::fs::remove_file(&path).unwrap();
|
||||
std::os::unix::fs::symlink("config/setting.json", &path).unwrap();
|
||||
assert!(
|
||||
dependency::execute_with("add", root, &["alpha@^1".into()], &services, &services).is_err()
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read(root.join("config/setting.json")).unwrap(),
|
||||
before
|
||||
);
|
||||
assert!(services.requests.borrow().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_reconciles_changed_requests_and_preserves_unrelated_settings() {
|
||||
let fixture = Fixture::new(&[]);
|
||||
let root = &fixture.directory.0;
|
||||
let mut services = Services::default();
|
||||
services.release("alpha", "1.0.0", &[]);
|
||||
services.release("alpha", "2.0.0", &[]);
|
||||
dependency::execute_with("add", root, &["alpha@^1".into()], &services, &services).unwrap();
|
||||
write(root, "config/setting.json", br#"{"package":{"registry":"https://packages.example.test","use":["alpha@^2"]},"business":{"secret":"unchanged"}}"#);
|
||||
dependency::execute_with("update", root, &[], &services, &services).unwrap();
|
||||
assert_eq!(versions(root)[0].1, "2.0.0");
|
||||
let setting: serde_json::Value =
|
||||
serde_json::from_slice(&std::fs::read(root.join("config/setting.json")).unwrap()).unwrap();
|
||||
assert_eq!(setting["business"]["secret"], "unchanged");
|
||||
}
|
||||
Reference in new issue
Block a user