208 lines
9.2 KiB
Rust
208 lines
9.2 KiB
Rust
//! The grammar consumer: grammars from the add-on store (single entries and packs) reach the
|
|
//! highlighter next to the pre-registry ones, and `grammar.*` use the shared installer.
|
|
|
|
mod addons_support;
|
|
mod common;
|
|
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use addons_support::{Shared, TINY, entry, index_json, serve};
|
|
use common::{TempVault, temp_config, temp_vault};
|
|
use notescraft_engine::addons::{FakeTransport, FakeVerifier, INDEX_URL, Response};
|
|
use notescraft_engine::{Error, Registry, Session, register_core};
|
|
use notescraft_syntax::load;
|
|
use serde_json::{Value, json};
|
|
|
|
/// The highlighter's grammar list is process-wide, so these tests take turns.
|
|
static TURN: Mutex<()> = Mutex::new(());
|
|
|
|
fn turn() -> std::sync::MutexGuard<'static, ()> {
|
|
TURN.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
|
|
}
|
|
|
|
const OTHER: &str = "%YAML 1.2\n---\nname: Other\nfile_extensions: [other]\nscope: source.other\ncontexts:\n main:\n - match: '\\bbar\\b'\n scope: keyword.control.other\n";
|
|
|
|
/// `(kind, id, [(path, bytes)])`, all at version 1.0.0.
|
|
type Served<'a> = (&'a str, &'a str, Vec<(&'a str, &'a [u8])>);
|
|
|
|
struct Fixture {
|
|
reg: Registry<Session>,
|
|
session: Session,
|
|
config: std::path::PathBuf,
|
|
_guards: (TempVault, TempVault),
|
|
}
|
|
|
|
/// A session whose index lists `entries` and whose transport serves `files`
|
|
/// (`(kind, id, [(path, bytes)])`, all at version 1.0.0).
|
|
fn fixture(name: &str, entries: &[Value], files: &[Served], verify: bool) -> Fixture {
|
|
let (vault, config) = (temp_vault(name), temp_config(name));
|
|
let mut session = Session::open(vault.path()).unwrap();
|
|
session.use_settings_dir(config.path());
|
|
let doc = index_json(entries, &[]);
|
|
let mut net = FakeTransport::new().with_response(INDEX_URL, Ok(Response { status: 200, etag: None, bytes: doc }));
|
|
for (kind, id, list) in files {
|
|
net = serve(net, kind, id, "1.0.0", list);
|
|
}
|
|
session.set_addons_transport(Box::new(Shared(Arc::new(net))));
|
|
if verify {
|
|
session.set_addons_verifier(Box::new(FakeVerifier { accept: true }));
|
|
}
|
|
let mut reg = Registry::new();
|
|
register_core(&mut reg).unwrap();
|
|
let config_path = config.path().to_path_buf();
|
|
Fixture { reg, session, config: config_path, _guards: (vault, config) }
|
|
}
|
|
|
|
impl Fixture {
|
|
fn run(&mut self, id: &str, params: &Value) -> Result<Value, Error> {
|
|
self.reg.run(id, &mut self.session, params)
|
|
}
|
|
|
|
fn ok(&mut self, id: &str, params: &Value) -> Value {
|
|
self.run(id, params).unwrap_or_else(|e| panic!("{id}: {e}"))
|
|
}
|
|
|
|
fn code(&mut self, id: &str, params: &Value) -> String {
|
|
match self.run(id, params) {
|
|
Err(Error::Command { source, .. }) => source.code,
|
|
other => panic!("{id}: expected a command error, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
fn installed(&mut self) -> Vec<(String, String)> {
|
|
let info = self.ok("grammar.info", &json!({}));
|
|
info["installed"].as_array().unwrap().iter().map(|g| (g["id"].as_str().unwrap().to_owned(), g["source"].as_str().unwrap().to_owned())).collect()
|
|
}
|
|
}
|
|
|
|
/// A grammar transport with no network.
|
|
#[derive(Debug)]
|
|
struct Offline;
|
|
|
|
impl notescraft_syntax::fetch::Transport for Offline {
|
|
fn get(&self, _url: &str, _max: usize) -> notescraft_syntax::Result<Vec<u8>> {
|
|
Err(notescraft_syntax::Error::Offline)
|
|
}
|
|
}
|
|
|
|
fn tiny_entry() -> Value {
|
|
entry("grammar", "tiny", "1.0.0", &[("tiny.sublime-syntax", TINY.as_bytes())], &[])
|
|
}
|
|
|
|
#[test]
|
|
fn a_registry_install_shows_in_grammar_info_and_reaches_the_highlighter() {
|
|
let _turn = turn();
|
|
let mut f = fixture("gc-info", &[tiny_entry()], &[("grammar", "tiny", vec![("tiny.sublime-syntax", TINY.as_bytes())])], true);
|
|
f.ok("registry.check", &json!({}));
|
|
f.ok("registry.install", &json!({ "kind": "grammar", "id": "tiny" }));
|
|
f.session.load_grammars();
|
|
assert!(load::is_known("tiny"));
|
|
assert_eq!(f.installed(), [("tiny".to_owned(), "registry".to_owned())]);
|
|
let label = f.ok("grammar.info", &json!({ "label": "tiny" }));
|
|
assert_eq!((label["label"]["installed"].clone(), label["label"]["bundled"].clone()), (json!(true), json!(false)));
|
|
f.ok("registry.remove", &json!({ "kind": "grammar", "id": "tiny" }));
|
|
f.session.load_grammars();
|
|
assert!(!load::is_known("tiny"), "removal returns the plain-text fallback");
|
|
}
|
|
|
|
#[test]
|
|
fn legacy_grammars_show_as_legacy_and_the_store_wins_a_repeated_id() {
|
|
let _turn = turn();
|
|
let mut f = fixture("gc-legacy", &[tiny_entry()], &[("grammar", "tiny", vec![("tiny.sublime-syntax", TINY.as_bytes())])], true);
|
|
f.ok("grammar.install_text", &json!({ "id": "tiny", "text": TINY }));
|
|
f.ok("grammar.install_text", &json!({ "id": "other", "text": OTHER }));
|
|
let mut legacy = f.installed();
|
|
legacy.sort();
|
|
assert_eq!(legacy, [("other".to_owned(), "legacy".to_owned()), ("tiny".to_owned(), "legacy".to_owned())]);
|
|
f.ok("registry.check", &json!({}));
|
|
f.ok("registry.install", &json!({ "kind": "grammar", "id": "tiny" }));
|
|
f.session.load_grammars();
|
|
assert!(load::is_known("tiny") && load::is_known("other"));
|
|
let sources = f.installed();
|
|
assert!(sources.contains(&("tiny".to_owned(), "registry".to_owned())) && sources.contains(&("other".to_owned(), "legacy".to_owned())), "{sources:?}");
|
|
assert!(f.config.join("grammars/tiny.sublime-syntax").exists(), "legacy files are never deleted by adoption");
|
|
}
|
|
|
|
#[test]
|
|
fn many_installs_in_one_frame_rebuild_the_highlighter_once() {
|
|
let _turn = turn();
|
|
let second = entry("grammar", "other", "1.0.0", &[("other.sublime-syntax", OTHER.as_bytes())], &[]);
|
|
let mut f = fixture(
|
|
"gc-once",
|
|
&[tiny_entry(), second],
|
|
&[("grammar", "tiny", vec![("tiny.sublime-syntax", TINY.as_bytes())]), ("grammar", "other", vec![("other.sublime-syntax", OTHER.as_bytes())])],
|
|
true,
|
|
);
|
|
f.ok("registry.check", &json!({}));
|
|
f.session.load_grammars();
|
|
let before = load::current().generation;
|
|
f.ok("registry.install", &json!({ "kind": "grammar", "id": "tiny" }));
|
|
f.ok("registry.install", &json!({ "kind": "grammar", "id": "other" }));
|
|
assert_eq!(load::current().generation, before, "the commands themselves do not rebuild");
|
|
f.session.load_grammars();
|
|
f.session.load_grammars();
|
|
assert_eq!(load::current().generation, before + 1);
|
|
}
|
|
|
|
#[test]
|
|
fn a_corrupt_store_grammar_is_skipped_and_the_rest_load() {
|
|
let _turn = turn();
|
|
let second = entry("grammar", "other", "1.0.0", &[("other.sublime-syntax", OTHER.as_bytes())], &[]);
|
|
let mut f = fixture(
|
|
"gc-corrupt",
|
|
&[tiny_entry(), second],
|
|
&[("grammar", "tiny", vec![("tiny.sublime-syntax", TINY.as_bytes())]), ("grammar", "other", vec![("other.sublime-syntax", OTHER.as_bytes())])],
|
|
true,
|
|
);
|
|
f.ok("registry.check", &json!({}));
|
|
f.ok("registry.install", &json!({ "kind": "grammar", "id": "tiny" }));
|
|
f.ok("registry.install", &json!({ "kind": "grammar", "id": "other" }));
|
|
let file = f.config.join("registry/store/grammar/tiny/1.0.0/tiny.sublime-syntax");
|
|
std::fs::write(&file, "not a grammar: [").unwrap();
|
|
f.session.load_grammars();
|
|
assert!(load::is_known("other") && !load::is_known("tiny"));
|
|
}
|
|
|
|
#[test]
|
|
fn grammar_install_uses_the_registry_when_the_id_is_indexed_and_grammar_remove_undoes_it() {
|
|
let _turn = turn();
|
|
let mut f = fixture("gc-cmd", &[tiny_entry()], &[("grammar", "tiny", vec![("tiny.sublime-syntax", TINY.as_bytes())])], true);
|
|
f.ok("registry.check", &json!({}));
|
|
let done = f.ok("grammar.install", &json!({ "id": "tiny" }));
|
|
assert_eq!((done["id"].clone(), done["source"].clone()), (json!("tiny"), json!("registry")));
|
|
assert!(f.config.join("registry/store/grammar/tiny/1.0.0").is_dir());
|
|
assert!(!f.config.join("grammars/tiny.sublime-syntax").exists(), "nothing goes to the legacy folder");
|
|
assert!(load::is_known("tiny"), "grammar.install reloads the highlighter");
|
|
assert_eq!(f.installed(), [("tiny".to_owned(), "registry".to_owned())]);
|
|
f.ok("grammar.remove", &json!({ "id": "tiny" }));
|
|
assert!(!load::is_known("tiny"));
|
|
assert_eq!(f.code("grammar.remove", &json!({ "id": "tiny" })), "not_found");
|
|
}
|
|
|
|
#[test]
|
|
fn a_build_with_no_trusted_key_keeps_the_legacy_flow() {
|
|
let _turn = turn();
|
|
let mut f = fixture("gc-nokey", &[tiny_entry()], &[("grammar", "tiny", vec![("tiny.sublime-syntax", TINY.as_bytes())])], false);
|
|
f.session.set_grammar_transport(Box::new(Offline));
|
|
f.ok("registry.check", &json!({}));
|
|
// The default verifier refuses with `addon`; the command falls through to the old flow, which
|
|
// here finds no network (a pasted-link style failure), not the registry's refusal.
|
|
assert_eq!(f.code("grammar.install", &json!({ "id": "tiny" })), "offline");
|
|
}
|
|
|
|
#[test]
|
|
fn a_pack_grammar_flows_to_the_highlighter() {
|
|
let _turn = turn();
|
|
let pack = entry("pack", "langs", "1.0.0", &[("grammars/tiny.sublime-syntax", TINY.as_bytes()), ("grammars/other.sublime-syntax", OTHER.as_bytes())], &[]);
|
|
let mut f = fixture(
|
|
"gc-pack",
|
|
&[pack],
|
|
&[("pack", "langs", vec![("grammars/tiny.sublime-syntax", TINY.as_bytes()), ("grammars/other.sublime-syntax", OTHER.as_bytes())])],
|
|
true,
|
|
);
|
|
f.ok("registry.check", &json!({}));
|
|
f.ok("registry.install", &json!({ "kind": "pack", "id": "langs" }));
|
|
f.session.load_grammars();
|
|
assert!(load::is_known("tiny") && load::is_known("other"));
|
|
}
|