chore: apply cargo fmt across workspace and update docs
- Run cargo fmt on all Rust crates for consistent formatting - Update CLAUDE.md with WASM plugin commands and dev.ps1 instructions - Update wiki: add WASM plugin architecture, rewrite dev environment docs - Minor frontend cleanup (unused imports)
This commit is contained in:
220
crates/erp-plugin-prototype/tests/test_plugin_integration.rs
Normal file
220
crates/erp-plugin-prototype/tests/test_plugin_integration.rs
Normal file
@@ -0,0 +1,220 @@
|
||||
//! WASM 插件集成测试
|
||||
//!
|
||||
//! 验证目标:
|
||||
//! V1 — WIT 接口 + bindgen! 编译通过
|
||||
//! V2 — Host 调用插件 init() / handle_event()
|
||||
//! V3 — 插件回调 Host db_insert / log_write
|
||||
//! V4 — async 实例化桥接
|
||||
//! V5 — Fuel 资源限制
|
||||
//! V6 — 从二进制动态加载
|
||||
|
||||
use anyhow::Result;
|
||||
use erp_plugin_prototype::{create_engine, load_plugin};
|
||||
|
||||
/// 获取测试插件 WASM Component 文件路径
|
||||
fn wasm_path() -> String {
|
||||
let candidates = [
|
||||
// 预构建的 WASM Component(通过 wasm-tools component new 生成)
|
||||
"../../target/erp_plugin_test_sample.component.wasm".into(),
|
||||
// 备选:绝对路径
|
||||
format!(
|
||||
"{}/../../target/erp_plugin_test_sample.component.wasm",
|
||||
std::env::current_dir().unwrap().display()
|
||||
),
|
||||
];
|
||||
for path in &candidates {
|
||||
if std::path::Path::new(path).exists() {
|
||||
return path.clone();
|
||||
}
|
||||
}
|
||||
candidates[0].clone()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_v6_load_plugin_from_binary() -> Result<()> {
|
||||
let wasm_path = wasm_path();
|
||||
let wasm_bytes = std::fs::read(&wasm_path).map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
"读取 WASM 失败: {}。请先编译: cargo build -p erp-plugin-test-sample --target wasm32-unknown-unknown --release\n路径: {}",
|
||||
e, wasm_path
|
||||
)
|
||||
})?;
|
||||
|
||||
assert!(!wasm_bytes.is_empty(), "WASM 文件不应为空");
|
||||
println!("WASM 文件大小: {} bytes", wasm_bytes.len());
|
||||
|
||||
let engine = create_engine()?;
|
||||
let (_store, _instance) = load_plugin(&engine, &wasm_bytes, 1_000_000).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_v2_host_calls_plugin_init() -> Result<()> {
|
||||
let wasm_bytes = std::fs::read(wasm_path())?;
|
||||
let engine = create_engine()?;
|
||||
let (mut store, instance) = load_plugin(&engine, &wasm_bytes, 1_000_000).await?;
|
||||
|
||||
// V2: 调用插件 init()
|
||||
instance
|
||||
.erp_plugin_plugin_api()
|
||||
.call_init(&mut store)?
|
||||
.map_err(|e| anyhow::anyhow!(e))?;
|
||||
|
||||
// 验证 Host 端收到了日志
|
||||
let state = store.data();
|
||||
assert!(
|
||||
state
|
||||
.logs
|
||||
.iter()
|
||||
.any(|(_, m)| m.contains("测试插件初始化成功")),
|
||||
"应收到初始化日志"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_v3_plugin_calls_host_api() -> Result<()> {
|
||||
let wasm_bytes = std::fs::read(wasm_path())?;
|
||||
let engine = create_engine()?;
|
||||
let (mut store, instance) = load_plugin(&engine, &wasm_bytes, 1_000_000).await?;
|
||||
|
||||
// 先 init(插件会调用 db_insert 和 log_write)
|
||||
instance
|
||||
.erp_plugin_plugin_api()
|
||||
.call_init(&mut store)?
|
||||
.map_err(|e| anyhow::anyhow!(e))?;
|
||||
|
||||
let state = store.data();
|
||||
|
||||
// 验证 db_insert 被调用
|
||||
assert!(
|
||||
state
|
||||
.db_ops
|
||||
.iter()
|
||||
.any(|op| op.op_type == "insert" && op.entity == "inventory_item"),
|
||||
"应有 inventory_item 的 insert 操作"
|
||||
);
|
||||
|
||||
// 验证 log_write 被调用
|
||||
assert!(
|
||||
state.logs.iter().any(|(_, m)| m.contains("插入成功")),
|
||||
"应有插入成功的日志"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_v3_plugin_handle_event_with_db_callback() -> Result<()> {
|
||||
let wasm_bytes = std::fs::read(wasm_path())?;
|
||||
let engine = create_engine()?;
|
||||
let (mut store, instance) = load_plugin(&engine, &wasm_bytes, 1_000_000).await?;
|
||||
|
||||
// 先 init
|
||||
instance
|
||||
.erp_plugin_plugin_api()
|
||||
.call_init(&mut store)?
|
||||
.map_err(|e| anyhow::anyhow!(e))?;
|
||||
|
||||
// 发送事件
|
||||
let payload = serde_json::json!({"order_id": "PO-001", "action": "approve"}).to_string();
|
||||
instance
|
||||
.erp_plugin_plugin_api()
|
||||
.call_handle_event(&mut store, "workflow.task.completed", payload.as_bytes())?
|
||||
.map_err(|e| anyhow::anyhow!(e))?;
|
||||
|
||||
let state = store.data();
|
||||
|
||||
// 验证 db_update 被调用
|
||||
assert!(
|
||||
state
|
||||
.db_ops
|
||||
.iter()
|
||||
.any(|op| op.op_type == "update" && op.entity == "purchase_order"),
|
||||
"应有 purchase_order 的 update 操作"
|
||||
);
|
||||
|
||||
// 验证事件被发布
|
||||
assert!(
|
||||
state
|
||||
.events
|
||||
.iter()
|
||||
.any(|(t, _)| t == "purchase_order.approved"),
|
||||
"应发布 purchase_order.approved 事件"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_v5_fuel_limit_traps() -> Result<()> {
|
||||
let wasm_bytes = std::fs::read(wasm_path())?;
|
||||
let engine = create_engine()?;
|
||||
|
||||
// 给极少量的 fuel,插件 init() 应该无法完成
|
||||
let result = load_plugin(&engine, &wasm_bytes, 10).await;
|
||||
match result {
|
||||
Ok((mut store, instance)) => {
|
||||
let init_result = instance.erp_plugin_plugin_api().call_init(&mut store);
|
||||
assert!(
|
||||
init_result.is_err() || init_result.unwrap().is_err(),
|
||||
"低 fuel 应导致失败"
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
// 实例化就失败了,也是预期行为
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_full_lifecycle() -> Result<()> {
|
||||
let wasm_bytes = std::fs::read(wasm_path())?;
|
||||
let engine = create_engine()?;
|
||||
let (mut store, instance) = load_plugin(&engine, &wasm_bytes, 1_000_000).await?;
|
||||
|
||||
// 1. init
|
||||
instance
|
||||
.erp_plugin_plugin_api()
|
||||
.call_init(&mut store)?
|
||||
.map_err(|e| anyhow::anyhow!(e))?;
|
||||
|
||||
// 2. on_tenant_created
|
||||
instance
|
||||
.erp_plugin_plugin_api()
|
||||
.call_on_tenant_created(&mut store, "tenant-new-001")?
|
||||
.map_err(|e| anyhow::anyhow!(e))?;
|
||||
|
||||
// 3. handle_event
|
||||
let payload = serde_json::json!({"order_id": "PO-002"}).to_string();
|
||||
instance
|
||||
.erp_plugin_plugin_api()
|
||||
.call_handle_event(&mut store, "workflow.task.completed", payload.as_bytes())?
|
||||
.map_err(|e| anyhow::anyhow!(e))?;
|
||||
|
||||
let state = store.data();
|
||||
|
||||
// 完整生命周期验证
|
||||
assert!(
|
||||
state.logs.len() >= 3,
|
||||
"应有多条日志记录,实际: {}",
|
||||
state.logs.len()
|
||||
);
|
||||
assert!(
|
||||
state.db_ops.len() >= 3,
|
||||
"应有多次数据库操作,实际: {}",
|
||||
state.db_ops.len()
|
||||
);
|
||||
assert!(state.events.len() >= 1, "应有事件发布");
|
||||
|
||||
println!("\n=== 完整生命周期验证 ===");
|
||||
println!("日志: {} 条", state.logs.len());
|
||||
println!("DB 操作: {} 次", state.db_ops.len());
|
||||
println!("事件: {} 条", state.events.len());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user