146 lines
6.9 KiB
Rust
146 lines
6.9 KiB
Rust
//! The vault `wanted` scan: fence labels and math in the notes become `wanted` ids in
|
|
//! `.notescraft/registry.json`; installed items are not listed; the budget ends a scan early;
|
|
//! nothing touches the network.
|
|
|
|
mod addons_support;
|
|
mod common;
|
|
|
|
use std::sync::{Arc, Mutex};
|
|
use std::time::Duration;
|
|
|
|
use addons_support::{Shared, TINY, entry, index_json, serve};
|
|
use common::{TempVault, seed, temp_config, temp_vault};
|
|
use notescraft_engine::addons::{FakeTransport, FakeVerifier, INDEX_URL, Response, VaultFile, record_wanted, record_wanted_once, scan_usage, scan_wanted};
|
|
use notescraft_engine::{Registry, Session, register_core};
|
|
use serde_json::{Value, json};
|
|
|
|
/// The highlighter's grammar list is process-wide, so these tests take turns.
|
|
static TURN: Mutex<()> = Mutex::new(());
|
|
|
|
/// A grammar no build bundles, so the scan treats its fence label as missing.
|
|
const ID: &str = "zorkscript";
|
|
const FONT: &[u8] = &[0, 1, 0, 0, 9, 9];
|
|
const REGISTRY_JSON: &str = ".notescraft/registry.json";
|
|
|
|
struct Fixture {
|
|
reg: Registry<Session>,
|
|
session: Session,
|
|
net: Arc<FakeTransport>,
|
|
vault: std::path::PathBuf,
|
|
_guards: (TempVault, TempVault),
|
|
}
|
|
|
|
fn fixture(name: &str, notes: &[(&str, &str)]) -> Fixture {
|
|
let (vault, config) = (temp_vault(name), temp_config(name));
|
|
seed(vault.path(), notes);
|
|
let file = format!("{ID}.sublime-syntax");
|
|
let entries = [entry("grammar", ID, "1.0.0", &[(&file, TINY.as_bytes())], &[]), entry("font", "stix-math", "1.0.0", &[("stix.ttf", FONT)], &[])];
|
|
let net = FakeTransport::new().with_response(INDEX_URL, Ok(Response { status: 200, etag: None, bytes: index_json(&entries, &[]) }));
|
|
let net = Arc::new(serve(serve(net, "grammar", ID, "1.0.0", &[(&file, TINY.as_bytes())]), "font", "stix-math", "1.0.0", &[("stix.ttf", FONT)]));
|
|
let mut session = Session::open(vault.path()).unwrap();
|
|
session.use_settings_dir(config.path());
|
|
session.set_addons_transport(Box::new(Shared(net.clone())));
|
|
session.set_addons_verifier(Box::new(FakeVerifier { accept: true }));
|
|
let mut reg = Registry::new();
|
|
register_core(&mut reg).unwrap();
|
|
let path = vault.path().to_path_buf();
|
|
Fixture { reg, session, net, vault: path, _guards: (vault, config) }
|
|
}
|
|
|
|
impl Fixture {
|
|
fn ok(&mut self, id: &str, params: &Value) -> Value {
|
|
self.reg.run(id, &mut self.session, params).unwrap_or_else(|e| panic!("{id}: {e}"))
|
|
}
|
|
}
|
|
|
|
fn fenced(label: &str) -> String {
|
|
format!("# Note\n\n```{label}\nfoo\n```\n")
|
|
}
|
|
|
|
#[test]
|
|
fn a_fence_label_of_an_indexed_grammar_is_wanted() {
|
|
let _turn = TURN.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
let mut f = fixture("wn-fence", &[("a.md", &fenced(ID)), ("b.md", &fenced("rust")), ("c.md", &fenced("nothing-indexed"))]);
|
|
assert_eq!(scan_wanted(&f.session, 5000).unwrap(), Vec::<String>::new(), "no cached index, no answer");
|
|
f.ok("registry.check", &json!({}));
|
|
assert_eq!(scan_wanted(&f.session, 5000).unwrap(), [format!("grammar:{ID}")]);
|
|
}
|
|
|
|
#[test]
|
|
fn a_label_only_counts_where_a_fence_opens() {
|
|
let _turn = TURN.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
let text = format!("```text\n```{ID}\n~~~{ID}\n```\n\nplain {ID}\n");
|
|
let mut f = fixture("wn-nest", &[("a.md", &text)]);
|
|
f.ok("registry.check", &json!({}));
|
|
assert!(scan_wanted(&f.session, 5000).unwrap().is_empty(), "the lookalike lines are inside a `text` fence");
|
|
}
|
|
|
|
#[test]
|
|
fn installed_items_are_not_listed() {
|
|
let _turn = TURN.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
let mut f = fixture("wn-installed", &[("a.md", &fenced(ID))]);
|
|
f.ok("registry.check", &json!({}));
|
|
f.ok("registry.install", &json!({ "kind": "grammar", "id": ID }));
|
|
assert_eq!(scan_wanted(&f.session, 5000).unwrap(), Vec::<String>::new());
|
|
f.ok("registry.remove", &json!({ "kind": "grammar", "id": ID }));
|
|
f.session.load_grammars();
|
|
assert_eq!(scan_wanted(&f.session, 5000).unwrap(), [format!("grammar:{ID}")]);
|
|
}
|
|
|
|
#[test]
|
|
fn math_wants_the_math_font_but_prices_do_not() {
|
|
let _turn = TURN.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
let mut f = fixture("wn-math", &[("price.md", "It costs $5 and then $6 more.\n")]);
|
|
f.ok("registry.check", &json!({}));
|
|
assert_eq!(scan_wanted(&f.session, 5000).unwrap(), Vec::<String>::new());
|
|
seed(&f.vault, &[("math.md", "Euler: $e^{i\\pi}+1=0$\n")]);
|
|
assert_eq!(scan_wanted(&f.session, 5000).unwrap(), ["font:stix-math"]);
|
|
seed(&f.vault, &[("math.md", "$$\nx\n$$\n")]);
|
|
assert_eq!(scan_wanted(&f.session, 5000).unwrap(), ["font:stix-math"]);
|
|
}
|
|
|
|
#[test]
|
|
fn the_budget_ends_a_big_scan_early_without_error() {
|
|
let _turn = TURN.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
let notes: Vec<(String, String)> = (0..400).map(|n| (format!("n{n}.md"), fenced(ID))).collect();
|
|
let refs: Vec<(&str, &str)> = notes.iter().map(|(a, b)| (a.as_str(), b.as_str())).collect();
|
|
let mut f = fixture("wn-budget", &refs);
|
|
f.ok("registry.check", &json!({}));
|
|
let cut = scan_usage(&f.vault, Duration::ZERO);
|
|
assert!(cut.cut_short && cut.labels.is_empty());
|
|
assert!(scan_wanted(&f.session, 0).unwrap().is_empty(), "a spent budget is an empty answer, not an error");
|
|
let full = scan_usage(&f.vault, Duration::from_secs(30));
|
|
assert!(!full.cut_short && full.labels.contains(ID));
|
|
}
|
|
|
|
#[test]
|
|
fn notes_over_two_mib_are_not_read() {
|
|
let _turn = TURN.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
let big = format!("{}{}", fenced(ID), "x".repeat(2 * 1024 * 1024));
|
|
let f = fixture("wn-big", &[("big.md", &big)]);
|
|
assert!(scan_usage(&f.vault, Duration::from_secs(30)).labels.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn record_writes_wanted_keeps_enabled_never_uses_the_network_and_runs_once_per_generation() {
|
|
let _turn = TURN.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
let mut f = fixture("wn-record", &[("a.md", &fenced(ID))]);
|
|
f.ok("registry.check", &json!({}));
|
|
let asked = f.net.asked().len();
|
|
seed(&f.vault, &[(REGISTRY_JSON, r#"{"version":1,"enabled":["plugin:dice"],"wanted":[]}"#)]);
|
|
assert_eq!(record_wanted_once(&f.session, 5000).unwrap(), Some(vec![format!("grammar:{ID}")]));
|
|
let file = VaultFile::parse(&std::fs::read(f.vault.join(REGISTRY_JSON)).unwrap()).unwrap();
|
|
assert_eq!((file.enabled, file.wanted), (vec!["plugin:dice".to_owned()], vec![format!("grammar:{ID}")]));
|
|
assert_eq!(record_wanted_once(&f.session, 5000).unwrap(), None, "same vault, same index generation");
|
|
assert_eq!(f.net.asked().len(), asked, "the scan never touches the network");
|
|
}
|
|
|
|
#[test]
|
|
fn a_corrupt_registry_json_is_left_untouched() {
|
|
let _turn = TURN.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
|
let mut f = fixture("wn-corrupt", &[("a.md", &fenced(ID)), (REGISTRY_JSON, "{ nope")]);
|
|
f.ok("registry.check", &json!({}));
|
|
assert!(record_wanted(&f.session, 5000).is_err());
|
|
assert_eq!(std::fs::read_to_string(f.vault.join(REGISTRY_JSON)).unwrap(), "{ nope");
|
|
}
|