Files
core/test/rust/native/dependency/commands.rs
T
2026-11-03 03:32:07 +00:00

362 lines
13 KiB
Rust

//! 使用自有 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");
}