172 lines
7.4 KiB
Rust
172 lines
7.4 KiB
Rust
//! `grammar.install`, `grammar.remove` and `grammar.info`, with a fake transport (no network).
|
|
|
|
mod common;
|
|
|
|
use std::collections::BTreeMap;
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use common::{temp_config, temp_vault};
|
|
use notescraft_engine::{AppSettings, Registry, Session, register_core};
|
|
use notescraft_syntax::Error as SyntaxError;
|
|
use notescraft_syntax::fetch::Transport;
|
|
use notescraft_syntax::validate::sha256_hex;
|
|
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 TINY: &str = "%YAML 1.2\n---\nname: Tiny\nfile_extensions: [tiny]\nscope: source.tiny\ncontexts:\n main:\n - match: '\\bfoo\\b'\n scope: keyword.control.tiny\n";
|
|
const RAW: &str = "https://git.marcospaulo.dev.br/masi/notescraft-registry/raw/branch/main/";
|
|
|
|
/// Serves canned bodies by URL and records every request.
|
|
#[derive(Debug, Clone, Default)]
|
|
struct Fake {
|
|
files: Arc<Mutex<BTreeMap<String, Vec<u8>>>>,
|
|
asked: Arc<Mutex<Vec<String>>>,
|
|
}
|
|
|
|
impl Transport for Fake {
|
|
fn get(&self, url: &str, _max: usize) -> notescraft_syntax::Result<Vec<u8>> {
|
|
self.asked.lock().expect("lock").push(url.to_owned());
|
|
self.files.lock().expect("lock").get(url).cloned().ok_or(SyntaxError::Offline)
|
|
}
|
|
}
|
|
|
|
fn index(sha: &str) -> String {
|
|
json!({ "version": 1, "grammars": [
|
|
{ "id": "tiny", "name": "Tiny", "format": "sublime-syntax", "file": "grammars/tiny.sublime-syntax", "sha256": sha, "license": "MIT", "sample": "foo\n" },
|
|
{ "id": "plist", "format": "tmLanguage", "file": "grammars/plist.tmLanguage", "sha256": "00" },
|
|
{ "id": "evil", "format": "sublime-syntax", "file": "../../etc/passwd", "sha256": "00" }
|
|
] })
|
|
.to_string()
|
|
}
|
|
|
|
struct Setup {
|
|
registry: Registry<Session>,
|
|
session: Session,
|
|
fake: Fake,
|
|
config: common::TempVault,
|
|
_vault: common::TempVault,
|
|
}
|
|
|
|
fn setup(name: &str, body: &str) -> Setup {
|
|
let vault = temp_vault(name);
|
|
let config = temp_config(name);
|
|
let mut registry = Registry::new();
|
|
register_core(&mut registry).expect("register");
|
|
let mut session = Session::open(vault.path()).expect("open");
|
|
session.use_settings_dir(config.path());
|
|
let fake = Fake::default();
|
|
{
|
|
let mut files = fake.files.lock().expect("lock");
|
|
files.insert(format!("{RAW}index.json"), index(&sha256_hex(body.as_bytes())).into_bytes());
|
|
files.insert(format!("{RAW}grammars/tiny.sublime-syntax"), body.as_bytes().to_vec());
|
|
}
|
|
session.set_grammar_transport(Box::new(fake.clone()));
|
|
Setup { registry, session, fake, config, _vault: vault }
|
|
}
|
|
|
|
fn run(s: &mut Setup, id: &str, params: &Value) -> Result<Value, notescraft_engine::Error> {
|
|
s.registry.run(id, &mut s.session, params)
|
|
}
|
|
|
|
fn installed(s: &Setup) -> AppSettings {
|
|
notescraft_engine::load_at(s.config.path()).0
|
|
}
|
|
|
|
#[test]
|
|
fn a_registry_install_writes_the_file_and_records_it() {
|
|
let _turn = turn();
|
|
let mut s = setup("gram-ok", TINY);
|
|
let result = run(&mut s, "grammar.install", &json!({ "id": "tiny" })).expect("installs");
|
|
assert_eq!(result["name"], json!("Tiny"));
|
|
assert_eq!(result["license"], json!("MIT"));
|
|
let file = s.config.path().join("grammars/tiny.sublime-syntax");
|
|
assert_eq!(std::fs::read_to_string(&file).expect("file"), TINY);
|
|
assert_eq!(installed(&s).grammars.iter().find(|g| g.id == "tiny").map(|g| g.name.as_str()), Some("Tiny"));
|
|
assert!(notescraft_syntax::load::is_known("tiny"), "the installed grammar is live in the highlighter");
|
|
let info = run(&mut s, "grammar.info", &json!({ "label": "pyhton" })).expect("info");
|
|
assert_eq!(info["installed"].as_array().map(Vec::len), Some(1));
|
|
assert_eq!(info["installed"][0]["source"], json!("legacy"));
|
|
assert_eq!(info["label"]["suggestion"], json!("python"));
|
|
assert_eq!(info["label"]["bundled"], json!(false));
|
|
}
|
|
|
|
#[test]
|
|
fn a_pasted_link_installs_without_a_checksum() {
|
|
let _turn = turn();
|
|
let mut s = setup("gram-url", TINY);
|
|
s.fake.files.lock().expect("lock").insert("https://example.org/my.sublime-syntax".to_owned(), TINY.as_bytes().to_vec());
|
|
run(&mut s, "grammar.install", &json!({ "id": "mine", "url": "https://example.org/my.sublime-syntax" })).expect("installs");
|
|
assert!(s.config.path().join("grammars/mine.sublime-syntax").exists());
|
|
}
|
|
|
|
#[test]
|
|
fn every_failed_gate_saves_nothing() {
|
|
let _turn = turn();
|
|
let mut s = setup("gram-bad", TINY);
|
|
let before = installed(&s);
|
|
for params in [
|
|
json!({ "id": "plist" }),
|
|
json!({ "id": "evil" }),
|
|
json!({ "id": "absent" }),
|
|
json!({ "id": "Bad Id" }),
|
|
json!({ "id": "x", "url": "http://example.org/a.sublime-syntax" }),
|
|
json!({ "id": "x", "url": "https://127.0.0.1/a.sublime-syntax" }),
|
|
json!({ "id": "x", "url": "https://example.org/offline.sublime-syntax" }),
|
|
] {
|
|
assert!(run(&mut s, "grammar.install", ¶ms).is_err(), "{params}");
|
|
}
|
|
s.fake.files.lock().expect("lock").insert(format!("{RAW}grammars/tiny.sublime-syntax"), b"tampered".to_vec());
|
|
assert!(run(&mut s, "grammar.install", &json!({ "id": "tiny" })).is_err(), "checksum mismatch");
|
|
assert_eq!(installed(&s), before);
|
|
assert!(!s.config.path().join("grammars").exists() || std::fs::read_dir(s.config.path().join("grammars")).expect("dir").next().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn remove_deletes_the_file_and_the_entry() {
|
|
let _turn = turn();
|
|
let mut s = setup("gram-remove", TINY);
|
|
run(&mut s, "grammar.install", &json!({ "id": "tiny" })).expect("installs");
|
|
run(&mut s, "grammar.remove", &json!({ "id": "tiny" })).expect("removes");
|
|
assert!(!s.config.path().join("grammars/tiny.sublime-syntax").exists());
|
|
assert_eq!(installed(&s).grammars, Vec::new());
|
|
assert!(!notescraft_syntax::load::is_known("tiny"), "removal unloads it");
|
|
assert!(run(&mut s, "grammar.remove", &json!({ "id": "tiny" })).is_err(), "second removal is an error");
|
|
}
|
|
|
|
#[test]
|
|
fn bad_entries_are_dropped_and_the_rest_sorted() {
|
|
let _turn = turn();
|
|
let entry = |id: &str, file: &str| notescraft_engine::GrammarInfo { id: id.into(), file: file.into(), ..Default::default() };
|
|
let mut settings = AppSettings {
|
|
grammars: vec![
|
|
entry("zed", "zed.sublime-syntax"),
|
|
entry("ok", "ok.sublime-syntax"),
|
|
entry("../x", "../x.sublime-syntax"),
|
|
entry("mismatch", "other.sublime-syntax"),
|
|
entry("ok", "ok.sublime-syntax"),
|
|
],
|
|
..AppSettings::default()
|
|
};
|
|
let notes = settings.validate();
|
|
assert_eq!(settings.grammars.iter().map(|g| g.id.as_str()).collect::<Vec<_>>(), ["ok", "zed"]);
|
|
assert!(notes.iter().any(|n| n.contains("dropped 3")), "{notes:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn install_text_checks_the_grammar_then_saves_it() {
|
|
let _turn = turn();
|
|
let mut s = setup("gram-text", TINY);
|
|
run(&mut s, "grammar.install_text", &json!({ "id": "tiny", "text": TINY, "license": "MIT" })).expect("installs");
|
|
assert!(s.config.path().join("grammars/tiny.sublime-syntax").exists());
|
|
assert!(notescraft_syntax::load::is_known("tiny"));
|
|
assert!(run(&mut s, "grammar.install_text", &json!({ "id": "bad", "text": "nope: [" })).is_err());
|
|
assert!(run(&mut s, "grammar.install_text", &json!({ "id": "bad", "text": "<?xml version=\"1.0\"?><plist/>" })).is_err());
|
|
assert!(!s.config.path().join("grammars/bad.sublime-syntax").exists());
|
|
}
|