Add demo_plugin and plugin host integration tests
Minimal in-tree add-on (DP_HELLO) validates registry, ribbon merge, and try_dispatch. Five new lib tests; EntryKind gains PartialEq for test helpers.
This commit is contained in:
parent
9f35456205
commit
82993bc1db
12 changed files with 214 additions and 2 deletions
|
|
@ -51,7 +51,7 @@ cargo build
|
||||||
cargo test --lib
|
cargo test --lib
|
||||||
```
|
```
|
||||||
|
|
||||||
No domain plugin is registered in this branch; existing core tests should pass unchanged.
|
A minimal **`demo_plugin`** add-on registers at compile time for smoke tests (`DP_HELLO` command, Demo Plugin ribbon tab). Remove or gate it before release if maintainers prefer zero in-tree add-ons.
|
||||||
|
|
||||||
## Review questions
|
## Review questions
|
||||||
|
|
||||||
|
|
|
||||||
36
docs/issue-78-comment.txt
Normal file
36
docs/issue-78-comment.txt
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
@HakanSeven12 @schoeller — following up with a concrete architecture proposal and an implementation ready for review.
|
||||||
|
|
||||||
|
### Proposal
|
||||||
|
|
||||||
|
I've drafted a **QGIS-style add-on model** on my fork:
|
||||||
|
|
||||||
|
- **Spec:** [docs/plugin-architecture.md](https://github.com/mf4633/OpenCADStudio/blob/feature/plugin-host/docs/plugin-architecture.md)
|
||||||
|
- **Scaffold:** [docs/plugin-template/](https://github.com/mf4633/OpenCADStudio/tree/feature/plugin-host/docs/plugin-template)
|
||||||
|
- **Framework PR:** #80 — **host only, no Storm Sewer in core**
|
||||||
|
|
||||||
|
**Three layers:** host core → add-on package (plugin.toml, ribbon, commands) → optional headless engine crate. Domain data lives on DWG entities (XDATA), not a proprietary project DB.
|
||||||
|
|
||||||
|
**Phase 1 (PR #80):** in-process plugins via inventory::submit!(PluginRegistration), HostSession API, per-document plugin state, command routing without editing commands.rs.
|
||||||
|
|
||||||
|
**Phase 2:** user install folder + dynamic .dll/.so with the same plugin.toml.
|
||||||
|
|
||||||
|
### Storm Sewer (separate)
|
||||||
|
|
||||||
|
Storm Sewer stays on a separate branch as the reference consumer — not in core: [feature/storm-sewer-module](https://github.com/mf4633/OpenCADStudio/tree/feature/storm-sewer-module).
|
||||||
|
|
||||||
|
### Re: script languages (@schoeller, #29)
|
||||||
|
|
||||||
|
Agree this shouldn't be either/or. Suggested sequencing:
|
||||||
|
|
||||||
|
1. Native Rust add-ons + stable HostSession / ocs_plugin_api
|
||||||
|
2. Python (or similar) as **bindings over that same API** — one extension surface, two authoring paths
|
||||||
|
|
||||||
|
Phase 1 defers embedded scripting until the native API is stable.
|
||||||
|
|
||||||
|
### Questions for maintainers
|
||||||
|
|
||||||
|
1. ocs_plugin_api as a workspace crate with semver — OK?
|
||||||
|
2. Should the main repo ship **zero** discipline modules, or optional built-ins for dev?
|
||||||
|
3. Priority: extract API crate (1b) vs dynamic loading (2)?
|
||||||
|
|
||||||
|
— Michael
|
||||||
|
|
@ -1475,6 +1475,22 @@ impl OpenCADStudio {
|
||||||
app
|
app
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn new_for_test() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn command_history_info(&self) -> Vec<String> {
|
||||||
|
use crate::ui::command_line::EntryKind;
|
||||||
|
self.command_line
|
||||||
|
.history
|
||||||
|
.iter()
|
||||||
|
.filter(|e| e.kind == EntryKind::Info)
|
||||||
|
.map(|e| e.text.clone())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Boot function for `iced::daemon`: returns initial state plus a task that
|
/// Boot function for `iced::daemon`: returns initial state plus a task that
|
||||||
/// opens the primary application window.
|
/// opens the primary application window.
|
||||||
fn boot() -> (Self, Task<Message>) {
|
fn boot() -> (Self, Task<Message>) {
|
||||||
|
|
|
||||||
11
src/modules/demo_plugin/dispatch.rs
Normal file
11
src/modules/demo_plugin/dispatch.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
use crate::plugin::host::HostSession;
|
||||||
|
|
||||||
|
pub fn handle(host: &mut HostSession<'_>, cmd: &str) -> bool {
|
||||||
|
match cmd {
|
||||||
|
"DP_HELLO" => {
|
||||||
|
host.push_info("Hello from demo_plugin (plugin host OK).");
|
||||||
|
true
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/modules/demo_plugin/manifest.rs
Normal file
14
src/modules/demo_plugin/manifest.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
use crate::plugin::manifest::{ApiVersion, PluginManifest};
|
||||||
|
|
||||||
|
pub const PLUGIN_ID: &str = "opencad.demo_plugin";
|
||||||
|
|
||||||
|
pub static MANIFEST: PluginManifest = PluginManifest {
|
||||||
|
id: PLUGIN_ID,
|
||||||
|
name: "Demo Plugin",
|
||||||
|
version: "0.1.0",
|
||||||
|
description: "Minimal add-on for plugin-host integration tests",
|
||||||
|
api_version: ApiVersion::CURRENT,
|
||||||
|
ribbon_order: 99,
|
||||||
|
xdata_apps: &[],
|
||||||
|
command_prefixes: &["DP_"],
|
||||||
|
};
|
||||||
47
src/modules/demo_plugin/mod.rs
Normal file
47
src/modules/demo_plugin/mod.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
// Minimal in-tree add-on — validates plugin host on `feature/plugin-host`.
|
||||||
|
|
||||||
|
pub mod dispatch;
|
||||||
|
pub mod manifest;
|
||||||
|
pub mod plugin;
|
||||||
|
pub mod register;
|
||||||
|
|
||||||
|
use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef};
|
||||||
|
|
||||||
|
inventory::submit!(crate::command::CommandRegistration {
|
||||||
|
names: &["DP_HELLO"]
|
||||||
|
});
|
||||||
|
|
||||||
|
pub struct DemoPluginModule;
|
||||||
|
|
||||||
|
impl CadModule for DemoPluginModule {
|
||||||
|
fn id(&self) -> &'static str {
|
||||||
|
"demo_plugin"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title(&self) -> &'static str {
|
||||||
|
"Demo Plugin"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ribbon_groups(&self) -> Vec<RibbonGroup> {
|
||||||
|
vec![RibbonGroup {
|
||||||
|
title: "Smoke",
|
||||||
|
tools: vec![RibbonItem::LargeTool(ToolDef {
|
||||||
|
id: "DP_HELLO",
|
||||||
|
label: "Hello",
|
||||||
|
icon: IconKind::Glyph("★"),
|
||||||
|
event: ModuleEvent::Command("DP_HELLO".to_string()),
|
||||||
|
})],
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::plugin::all_ribbon_modules;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ribbon_tab_is_registered() {
|
||||||
|
let titles: Vec<&str> = all_ribbon_modules().iter().map(|m| m.title()).collect();
|
||||||
|
assert!(titles.contains(&"Demo Plugin"), "tabs: {titles:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/modules/demo_plugin/plugin.rs
Normal file
21
src/modules/demo_plugin/plugin.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
use crate::plugin::host::{BuiltinPlugin, HostSession};
|
||||||
|
use crate::plugin::manifest::PluginManifest;
|
||||||
|
|
||||||
|
use super::dispatch;
|
||||||
|
use super::manifest;
|
||||||
|
|
||||||
|
pub struct DemoPlugin;
|
||||||
|
|
||||||
|
impl BuiltinPlugin for DemoPlugin {
|
||||||
|
fn manifest(&self) -> &'static PluginManifest {
|
||||||
|
&manifest::MANIFEST
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ribbon(&self) -> Box<dyn crate::modules::CadModule> {
|
||||||
|
Box::new(super::DemoPluginModule)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatch(&self, host: &mut HostSession<'_>, cmd: &str) -> bool {
|
||||||
|
dispatch::handle(host, cmd)
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/modules/demo_plugin/plugin.toml
Normal file
13
src/modules/demo_plugin/plugin.toml
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
[plugin]
|
||||||
|
id = "opencad.demo_plugin"
|
||||||
|
name = "Demo Plugin"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Minimal add-on for plugin-host integration tests"
|
||||||
|
author = "Open CAD Studio contributors"
|
||||||
|
license = "GPL-3.0-only"
|
||||||
|
|
||||||
|
[opencad]
|
||||||
|
api_version = 1
|
||||||
|
ribbon_order = 99
|
||||||
|
command_prefixes = ["DP_"]
|
||||||
|
xdata_apps = []
|
||||||
7
src/modules/demo_plugin/register.rs
Normal file
7
src/modules/demo_plugin/register.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
use super::plugin::DemoPlugin;
|
||||||
|
|
||||||
|
inventory::submit! {
|
||||||
|
crate::plugin::registry::PluginRegistration {
|
||||||
|
construct: || Box::new(DemoPlugin),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -139,6 +139,7 @@ pub mod home;
|
||||||
pub mod insert;
|
pub mod insert;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
|
pub mod demo_plugin;
|
||||||
pub mod manage;
|
pub mod manage;
|
||||||
pub mod view;
|
pub mod view;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,3 +40,49 @@ pub(crate) fn try_dispatch(app: &mut OpenCADStudio, tab: usize, cmd: &str) -> bo
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::app::OpenCADStudio;
|
||||||
|
#[test]
|
||||||
|
fn discovers_registered_plugins() {
|
||||||
|
let plugins = all_plugins();
|
||||||
|
assert!(
|
||||||
|
!plugins.is_empty(),
|
||||||
|
"expected at least one PluginRegistration (demo_plugin)"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
plugins
|
||||||
|
.iter()
|
||||||
|
.any(|p| p.manifest().id == "opencad.demo_plugin"),
|
||||||
|
"demo_plugin missing; ids: {:?}",
|
||||||
|
plugins.iter().map(|p| p.manifest().id).collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn addon_ribbon_tabs_merge_after_core() {
|
||||||
|
let titles: Vec<&str> = all_ribbon_modules().iter().map(|m| m.title()).collect();
|
||||||
|
assert!(titles.contains(&"Demo Plugin"), "ribbon tabs: {titles:?}");
|
||||||
|
let core = core_registry::all_modules();
|
||||||
|
assert_eq!(titles.len(), core.len() + all_plugins().len());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn try_dispatch_routes_demo_command() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
assert!(try_dispatch(&mut app, 0, "DP_HELLO"));
|
||||||
|
let info = app.command_history_info();
|
||||||
|
assert!(
|
||||||
|
info.iter().any(|t| t.contains("demo_plugin") && t.contains("plugin host OK")),
|
||||||
|
"info history: {info:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_plugin_command_falls_through() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
assert!(!try_dispatch(&mut app, 0, "DP_NOPE"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -49,7 +49,7 @@ pub struct HistoryEntry {
|
||||||
pub created_at: Instant,
|
pub created_at: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub enum EntryKind {
|
pub enum EntryKind {
|
||||||
Command,
|
Command,
|
||||||
Output,
|
Output,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue