a31eb59678
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
131 lines
5.1 KiB
Rust
131 lines
5.1 KiB
Rust
//! 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<Vec<String>>,
|
|
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<String> {
|
|
self.calls.lock().unwrap_or_else(PoisonError::into_inner).clone()
|
|
}
|
|
|
|
fn record(&self, call: &str, args: &[&str]) -> std::result::Result<String, HostError> {
|
|
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<String, HostError> {
|
|
self.record("log", &[level, message])
|
|
}
|
|
fn vault_read(&self, path: &str) -> std::result::Result<String, HostError> {
|
|
self.record("vault_read", &[path])
|
|
}
|
|
fn vault_write(&self, path: &str, content: &str) -> std::result::Result<String, HostError> {
|
|
self.record("vault_write", &[path, content])
|
|
}
|
|
fn vault_list(&self, glob: &str) -> std::result::Result<String, HostError> {
|
|
self.record("vault_list", &[glob])
|
|
}
|
|
fn search(&self, query: &str) -> std::result::Result<String, HostError> {
|
|
self.record("search", &[query])
|
|
}
|
|
fn links(&self, path: &str) -> std::result::Result<String, HostError> {
|
|
self.record("links", &[path])
|
|
}
|
|
fn backlinks(&self, path: &str) -> std::result::Result<String, HostError> {
|
|
self.record("backlinks", &[path])
|
|
}
|
|
fn active_note(&self) -> std::result::Result<String, HostError> {
|
|
self.record("active_note", &[])
|
|
}
|
|
fn insert_text(&self, text: &str) -> std::result::Result<String, HostError> {
|
|
self.record("insert_text", &[text])
|
|
}
|
|
fn notice(&self, text: &str) -> std::result::Result<String, HostError> {
|
|
self.record("notice", &[text])
|
|
}
|
|
fn now(&self) -> std::result::Result<String, HostError> {
|
|
self.record("now", &[])
|
|
}
|
|
}
|
|
|
|
/// Compiles a module from `tests/fixtures`.
|
|
#[must_use]
|
|
pub fn wasm(name: &str) -> Vec<u8> {
|
|
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::<Vec<_>>().join(",");
|
|
let cmds = commands.iter().map(|c| format!(r#"{{"id":"{c}","name":"{c} label"}}"#)).collect::<Vec<_>>().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<StubHost>) -> Result<Plugin> {
|
|
Plugin::load(manifest_for(id, permissions, commands), &wasm(name), host, Limits::default())
|
|
}
|