144 lines
6.2 KiB
Rust
144 lines
6.2 KiB
Rust
//! State file, per-vault file and store layout of the add-on registry.
|
|
|
|
mod common;
|
|
|
|
use notescraft_engine::addons::{Installed, Kind, Source, State, Store, VaultFile, Version, key};
|
|
|
|
fn v(major: u16, minor: u16, patch: u16) -> Version {
|
|
Version { major, minor, patch }
|
|
}
|
|
|
|
fn row(version: Version, previous: Option<Version>) -> Installed {
|
|
Installed { version, previous, source: Source::Registry, installed_at: "2026-10-21T12:00:00Z".into(), sha256_of_files: "e".repeat(64) }
|
|
}
|
|
|
|
fn sample() -> State {
|
|
let mut state = State::default();
|
|
state.installed.insert(key(Kind::Grammar, "twig"), row(v(1, 1, 0), Some(v(1, 0, 0))));
|
|
state.installed.insert(key(Kind::Plugin, "dice"), row(v(0, 1, 0), None));
|
|
state
|
|
}
|
|
|
|
#[test]
|
|
fn state_round_trip() {
|
|
let state = sample();
|
|
assert_eq!(State::parse(state.to_json().unwrap().as_bytes()).unwrap(), state);
|
|
}
|
|
|
|
#[test]
|
|
fn state_rejections() {
|
|
assert!(State::parse(b"nope").is_err());
|
|
assert!(State::parse(br#"{"version":2,"installed":{}}"#).is_err());
|
|
assert!(State::parse(br#"{"installed":{}}"#).is_err());
|
|
let good = |k: &str| {
|
|
format!(r#"{{"version":1,"installed":{{"{k}":{{"version":"1.0.0","source":"seed","installed_at":"t","sha256_of_files":"{}"}}}}}}"#, "e".repeat(64))
|
|
};
|
|
assert!(State::parse(good("grammar:twig").as_bytes()).is_ok());
|
|
for bad in ["twig", "exe:twig", "grammar:../x", "grammar:Twig", "grammar:"] {
|
|
assert!(State::parse(good(bad).as_bytes()).is_err(), "{bad}");
|
|
}
|
|
assert!(State::parse(good("grammar:a").replace("1.0.0", "1.0").as_bytes()).is_err());
|
|
assert!(State::parse(good("grammar:a").replace("seed", "magic").as_bytes()).is_err());
|
|
let mut big = State::default();
|
|
for i in 0..513 {
|
|
big.installed.insert(key(Kind::Theme, &format!("t{i}")), row(v(1, 0, 0), None));
|
|
}
|
|
assert!(big.to_json().is_err());
|
|
let rows: Vec<String> =
|
|
(0..513).map(|i| format!(r#""theme:t{i}":{{"version":"1.0.0","source":"seed","installed_at":"t","sha256_of_files":"{}"}}"#, "e".repeat(64))).collect();
|
|
assert!(State::parse(format!(r#"{{"version":1,"installed":{{{}}}}}"#, rows.join(",")).as_bytes()).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn vault_file_round_trip_and_caps() {
|
|
let file = VaultFile { enabled: vec!["plugin:dice".into()], wanted: vec!["grammar:twig".into(), "theme:x".into()], ..VaultFile::default() };
|
|
assert_eq!(VaultFile::parse(file.to_json().unwrap().as_bytes()).unwrap(), file);
|
|
assert!(VaultFile::parse(br#"{"version":2}"#).is_err());
|
|
assert!(VaultFile::parse(br#"{"version":1,"enabled":["grammar:twig"]}"#).is_err());
|
|
assert!(VaultFile::parse(br#"{"version":1,"wanted":["../x"]}"#).is_err());
|
|
let many: Vec<String> = (0..257).map(|i| format!("grammar:g{i}")).collect();
|
|
assert!(VaultFile { wanted: many, ..VaultFile::default() }.to_json().is_err());
|
|
assert_eq!(VaultFile::parse(br#"{"version":1}"#).unwrap(), VaultFile::default());
|
|
}
|
|
|
|
#[test]
|
|
fn commit_and_load() {
|
|
let dir = common::temp_config("addons-commit");
|
|
let store = Store::open(dir.path()).unwrap();
|
|
assert_eq!(store.load().unwrap(), State::default());
|
|
let state = sample();
|
|
store.commit(&state).unwrap();
|
|
assert_eq!(store.load().unwrap(), state);
|
|
assert!(store.root().join("state.json").is_file());
|
|
}
|
|
|
|
#[test]
|
|
fn corrupt_state_errs_and_file_is_untouched() {
|
|
let dir = common::temp_config("addons-corrupt");
|
|
let store = Store::open(dir.path()).unwrap();
|
|
let path = store.root().join("state.json");
|
|
std::fs::write(&path, b"{ not json").unwrap();
|
|
assert!(store.load().is_err());
|
|
assert!(store.gc(&State::default()).is_ok());
|
|
assert_eq!(std::fs::read(&path).unwrap(), b"{ not json");
|
|
}
|
|
|
|
#[test]
|
|
fn version_dir_uses_validated_parts() {
|
|
let dir = common::temp_config("addons-vdir");
|
|
let store = Store::open(dir.path()).unwrap();
|
|
let path = store.version_dir(Kind::Grammar, "twig", v(1, 2, 3)).unwrap();
|
|
assert!(path.starts_with(store.root()));
|
|
assert!(path.ends_with("store/grammar/twig/1.2.3"));
|
|
assert!(store.version_dir(Kind::Grammar, "../x", v(1, 0, 0)).is_err());
|
|
assert!(store.version_dir(Kind::Grammar, "a/b", v(1, 0, 0)).is_err());
|
|
assert!(store.version_dir(Kind::Grammar, "", v(1, 0, 0)).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn gc_keeps_version_and_previous_only() {
|
|
let dir = common::temp_config("addons-gc");
|
|
let store = Store::open(dir.path()).unwrap();
|
|
let state = sample();
|
|
let touch = |kind, id, ver| {
|
|
let d = store.version_dir(kind, id, ver).unwrap();
|
|
std::fs::create_dir_all(&d).unwrap();
|
|
std::fs::write(d.join("f"), b"x").unwrap();
|
|
d
|
|
};
|
|
let active = touch(Kind::Grammar, "twig", v(1, 1, 0));
|
|
let previous = touch(Kind::Grammar, "twig", v(1, 0, 0));
|
|
let old = touch(Kind::Grammar, "twig", v(0, 9, 0));
|
|
let orphan = touch(Kind::Theme, "gone", v(1, 0, 0));
|
|
let plugin = touch(Kind::Plugin, "dice", v(0, 1, 0));
|
|
std::fs::create_dir_all(store.staging_dir().join("abc")).unwrap();
|
|
std::fs::write(store.staging_dir().join("abc/part"), b"x").unwrap();
|
|
std::fs::write(store.staging_dir().join("loose"), b"x").unwrap();
|
|
let outside = dir.path().join("outside");
|
|
std::fs::create_dir_all(&outside).unwrap();
|
|
std::fs::write(outside.join("keep"), b"x").unwrap();
|
|
|
|
assert_eq!(store.gc(&state).unwrap(), 2);
|
|
assert!(active.is_dir() && previous.is_dir() && plugin.is_dir());
|
|
assert!(!old.exists() && !orphan.exists());
|
|
assert!(!orphan.parent().unwrap().exists(), "empty parents are pruned");
|
|
assert_eq!(std::fs::read_dir(store.staging_dir()).unwrap().count(), 0);
|
|
assert!(outside.join("keep").is_file());
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn gc_does_not_follow_links() {
|
|
let dir = common::temp_config("addons-link");
|
|
let store = Store::open(dir.path()).unwrap();
|
|
let outside = dir.path().join("outside");
|
|
std::fs::create_dir_all(&outside).unwrap();
|
|
std::fs::write(outside.join("keep"), b"x").unwrap();
|
|
let parent = store.version_dir(Kind::Theme, "t", v(1, 0, 0)).unwrap().parent().unwrap().to_path_buf();
|
|
std::fs::create_dir_all(&parent).unwrap();
|
|
std::os::unix::fs::symlink(&outside, parent.join("2.0.0")).unwrap();
|
|
store.gc(&State::default()).unwrap();
|
|
assert!(outside.join("keep").is_file());
|
|
assert!(!parent.join("2.0.0").exists());
|
|
}
|