//! Shared fixtures for the plugin tests: a stub [`HostApi`] and the WAT modules under test. //! //! Each test binary uses a different subset of these helpers. #![allow(dead_code)] use std::sync::{Arc, Mutex, PoisonError}; use notescraft_plugins::{HostApi, HostError, Limits, Manifest, Plugin, Result}; /// A host that records every call and answers with a canned JSON payload. #[derive(Debug)] pub struct StubHost { calls: Mutex>, payload: String, failing: bool, } impl Default for StubHost { fn default() -> Self { Self::new() } } impl StubHost { /// Answers every call with `{"ok":true}`. #[must_use] pub fn new() -> Self { Self { calls: Mutex::new(Vec::new()), payload: String::from("{\"ok\":true}"), failing: false } } /// Fails every call. #[must_use] pub fn failing() -> Self { Self { failing: true, ..Self::new() } } /// Answers every call with `payload`. #[must_use] pub fn with_payload(payload: &str) -> Self { Self { payload: payload.to_string(), ..Self::new() } } /// The calls made so far, in order. #[must_use] pub fn calls(&self) -> Vec { self.calls.lock().unwrap_or_else(PoisonError::into_inner).clone() } fn record(&self, call: &str, args: &[&str]) -> std::result::Result { self.calls.lock().unwrap_or_else(PoisonError::into_inner).push(format!("{call}({})", args.join(","))); if self.failing { Err(HostError::new("the vault is locked")) } else { Ok(self.payload.clone()) } } } impl HostApi for StubHost { fn log(&self, level: &str, message: &str) -> std::result::Result { self.record("log", &[level, message]) } fn vault_read(&self, path: &str) -> std::result::Result { self.record("vault_read", &[path]) } fn vault_write(&self, path: &str, content: &str) -> std::result::Result { self.record("vault_write", &[path, content]) } fn vault_list(&self, glob: &str) -> std::result::Result { self.record("vault_list", &[glob]) } fn search(&self, query: &str) -> std::result::Result { self.record("search", &[query]) } fn links(&self, path: &str) -> std::result::Result { self.record("links", &[path]) } fn backlinks(&self, path: &str) -> std::result::Result { self.record("backlinks", &[path]) } fn active_note(&self) -> std::result::Result { self.record("active_note", &[]) } fn insert_text(&self, text: &str) -> std::result::Result { self.record("insert_text", &[text]) } fn notice(&self, text: &str) -> std::result::Result { self.record("notice", &[text]) } fn now(&self) -> std::result::Result { self.record("now", &[]) } } /// Compiles a module from `tests/fixtures`. #[must_use] pub fn wasm(name: &str) -> Vec { let text = match name { "good" => include_str!("../fixtures/good.wat"), "write" => include_str!("../fixtures/write.wat"), "infinite_loop" => include_str!("../fixtures/infinite_loop.wat"), "memory_bomb" => include_str!("../fixtures/memory_bomb.wat"), "oob_pointer" => include_str!("../fixtures/oob_pointer.wat"), "big_result" => include_str!("../fixtures/big_result.wat"), "missing_export" => include_str!("../fixtures/missing_export.wat"), "bad_abi" => include_str!("../fixtures/bad_abi.wat"), "extra_import" => include_str!("../fixtures/extra_import.wat"), "extra_ns_import" => include_str!("../fixtures/extra_ns_import.wat"), "trap" => include_str!("../fixtures/trap.wat"), "recursion" => include_str!("../fixtures/recursion.wat"), "guest_error" => include_str!("../fixtures/guest_error.wat"), "host_storm" => include_str!("../fixtures/host_storm.wat"), "huge_host_answer" => include_str!("../fixtures/huge_host_answer.wat"), other => panic!("no fixture named {other}"), }; wat::parse_str(text).unwrap_or_else(|e| panic!("fixture {name} is not valid WAT: {e}")) } /// A manifest for plugin `id` asking for `permissions` and declaring `commands`. pub fn manifest_for(id: &str, permissions: &[&str], commands: &[&str]) -> Manifest { let list = |items: &[&str]| items.iter().map(|i| format!("\"{i}\"")).collect::>().join(","); let cmds = commands.iter().map(|c| format!(r#"{{"id":"{c}","name":"{c} label"}}"#)).collect::>().join(","); let json = format!( r#"{{"id":"{id}","name":"Test plugin","version":"1.0.0","min_app_version":"0.1.0","permissions":[{}],"commands":[{}]}}"#, list(permissions), cmds ); Manifest::from_json(&json).unwrap_or_else(|e| panic!("{e}")) } /// Loads `name` as plugin `id` with the default limits. pub fn load(id: &str, name: &str, permissions: &[&str], commands: &[&str], host: Arc) -> Result { Plugin::load(manifest_for(id, permissions, commands), &wasm(name), host, Limits::default()) }