160 lines
6.8 KiB
Rust
160 lines
6.8 KiB
Rust
//! The first first-party plugin: the dice module, loaded from disk exactly as
|
|
//! a user plugin is (SPEC 7.2, 7.4).
|
|
//!
|
|
//! The build is not part of the test: run `plugins/dice/build.sh` first. The
|
|
//! tests are `#[ignore]`d, so a checkout without the module keeps the suite
|
|
//! green, and they print why they stop when the module is missing.
|
|
|
|
mod common;
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use common::{seed, temp_vault};
|
|
use notescraft_engine::{AppSettings, Buffer, Registry, Session, register_core};
|
|
use notescraft_plugins::Permission;
|
|
use serde_json::{Value, json};
|
|
|
|
/// The folder of the plugin, relative to the crate.
|
|
fn plugin_dir() -> PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../plugins/dice")
|
|
}
|
|
|
|
/// The module and the manifest of the plugin, or `None` when it was not built.
|
|
fn built() -> Option<(PathBuf, PathBuf)> {
|
|
let module = plugin_dir().join("dice.wasm");
|
|
let manifest = plugin_dir().join("manifest.json");
|
|
(module.exists() && manifest.exists()).then_some((module, manifest))
|
|
}
|
|
|
|
/// The settings that enable the plugin and approve the `editor` permission.
|
|
fn settings() -> AppSettings {
|
|
let mut settings = AppSettings::default();
|
|
settings.plugins.enabled.insert("dice".to_owned());
|
|
settings.plugins.approved.insert("dice".to_owned(), vec![Permission::Editor]);
|
|
settings
|
|
}
|
|
|
|
/// Runs `body` in a session whose vault holds the dice plugin.
|
|
fn with_dice<T>(body: impl FnOnce(&mut Session) -> T) -> Option<T> {
|
|
let Some((module, manifest)) = built() else {
|
|
println!("skipped: {} is missing; run plugins/dice/build.sh", plugin_dir().join("dice.wasm").display());
|
|
return None;
|
|
};
|
|
let vault = temp_vault("dice");
|
|
seed(vault.path(), &[("note.md", "roll: ")]);
|
|
let folder = vault.path().join(".notescraft/plugins/dice");
|
|
std::fs::create_dir_all(&folder).expect("plugin folder");
|
|
std::fs::copy(&module, folder.join("plugin.wasm")).expect("copy the module");
|
|
std::fs::copy(&manifest, folder.join("manifest.json")).expect("copy the manifest");
|
|
|
|
let mut session = Session::open(vault.path()).expect("session");
|
|
session.apply_app_settings(&settings()).expect("settings");
|
|
session.load_plugins().expect("load plugins");
|
|
Some(body(&mut session))
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "needs plugins/dice/dice.wasm: run plugins/dice/build.sh"]
|
|
fn the_plugin_loads_and_rolls() {
|
|
let Some(result) = with_dice(|session| {
|
|
// The host refuses a module whose `ns_abi_version` is not 1 and every
|
|
// missing export, so an empty diagnostics list is the ABI check.
|
|
assert!(session.diagnostics().is_empty(), "{:?}", session.diagnostics());
|
|
let infos = session.plugin_infos();
|
|
assert_eq!(infos.len(), 1);
|
|
assert_eq!(infos.first().map(|info| info.id.as_str()), Some("dice"));
|
|
|
|
// The manifest of the plugin is the one that ships: it declares `roll`.
|
|
let mut registry = Registry::new();
|
|
register_core(&mut registry).expect("core commands");
|
|
assert_eq!(session.register_plugin_commands(&mut registry).expect("register"), 1);
|
|
assert!(registry.get("plugin.dice.roll").is_some());
|
|
|
|
session.run_plugin_command("plugin.dice.roll", r#"{"expr":"1d1"}"#).expect("the plugin rolls")
|
|
}) else {
|
|
return;
|
|
};
|
|
let value: Value = serde_json::from_str(&result).expect("the answer is JSON");
|
|
assert_eq!(value.get("total"), Some(&json!(1)));
|
|
assert_eq!(value.get("rolls"), Some(&json!([1])));
|
|
assert_eq!(value.get("text"), Some(&json!("1d1 = 1 [1]")));
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "needs plugins/dice/dice.wasm: run plugins/dice/build.sh"]
|
|
fn the_roll_is_inserted_into_the_focused_note() {
|
|
let Some(text) = with_dice(|session| {
|
|
session.open_buffer("note.md").expect("buffer");
|
|
session.focus_note("note.md").expect("focus");
|
|
let result = session.run_plugin_command("plugin.dice.roll", r#"{"expr":"2d6+3"}"#).expect("the plugin rolls");
|
|
let value: Value = serde_json::from_str(&result).expect("the answer is JSON");
|
|
let text = value.get("text").and_then(Value::as_str).expect("the answer has a text").to_owned();
|
|
let buffer = session.buffer("note.md").map(Buffer::text).expect("the buffer is open").to_owned();
|
|
assert_eq!(buffer, format!("roll: {text}"), "the host inserted the text");
|
|
assert_eq!(buffer.matches(&text).count(), 1, "the text is inserted once");
|
|
text
|
|
}) else {
|
|
return;
|
|
};
|
|
assert!(text.starts_with("2d6+3 = "), "{text}");
|
|
assert!(text.ends_with(" +3"), "{text}");
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "needs plugins/dice/dice.wasm: run plugins/dice/build.sh"]
|
|
fn a_hundred_dice_stay_inside_the_fuel_budget() {
|
|
let Some(result) = with_dice(|session| {
|
|
// The default budget is 50 million fuel; a hundred dice must not come
|
|
// close to it, so the call has to answer instead of hitting the limit.
|
|
session.run_plugin_command("plugin.dice.roll", r#"{"expr":"100d1000"}"#).expect("the plugin rolls")
|
|
}) else {
|
|
return;
|
|
};
|
|
let value: Value = serde_json::from_str(&result).expect("the answer is JSON");
|
|
let rolls = value.get("rolls").and_then(Value::as_array).expect("the answer has the rolls");
|
|
assert_eq!(rolls.len(), 100);
|
|
let mut sum = 0u64;
|
|
for die in rolls {
|
|
let sides = die.as_u64().expect("a die is a number");
|
|
assert!((1..=1000).contains(&sides), "{sides}");
|
|
sum = sum.saturating_add(sides);
|
|
}
|
|
let total = value.get("total").and_then(Value::as_i64).expect("the answer has the total");
|
|
assert_eq!(total, i64::try_from(sum).expect("the sum fits"));
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "needs plugins/dice/dice.wasm: run plugins/dice/build.sh"]
|
|
fn an_expression_out_of_bounds_is_a_guest_error() {
|
|
// Three refusals are the most a plugin may collect before the host
|
|
// disables it, so this session runs exactly three.
|
|
let Some(errors) = with_dice(|session| {
|
|
let mut errors = Vec::new();
|
|
for expr in ["0d6", "101d6", "1d1001"] {
|
|
let args = format!(r#"{{"expr":"{expr}"}}"#);
|
|
let error = session.run_plugin_command("plugin.dice.roll", &args).expect_err("the expression is refused");
|
|
errors.push(error.to_string());
|
|
}
|
|
errors
|
|
}) else {
|
|
return;
|
|
};
|
|
for error in &errors {
|
|
assert!(error.contains("error code"), "{error}");
|
|
assert!(!error.contains("trap"), "{error}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "needs plugins/dice/dice.wasm: run plugins/dice/build.sh"]
|
|
fn a_kept_expression_is_a_guest_error() {
|
|
let Some(error) = with_dice(|session| {
|
|
let error = session.run_plugin_command("plugin.dice.roll", r#"{"expr":"2d6kh3"}"#).expect_err("kh is not supported");
|
|
error.to_string()
|
|
}) else {
|
|
return;
|
|
};
|
|
assert!(error.contains("error code"), "{error}");
|
|
assert!(!error.contains("trap"), "{error}");
|
|
}
|