Files
hms/crates/erp-workflow/src/engine/model.rs
iven dde6b09017
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
test(workflow): erp-workflow 单元测试从 16 增至 63 — 覆盖 model/error/parser/expression/executor
- model.rs: 13 个 FlowGraph 测试(build/outgoing/incoming/start-end 边界)
- error.rs: 7 个 WorkflowError → AppError 转换测试
- parser.rs: 11 个新增验证边界测试(空图/多起点/幽灵边/网关约束)
- expression.rs: 13 个新增求值测试(float/bool/string/复合表达式/空白容错)
- executor.rs: 3 个 is_join_gateway 纯函数测试
2026-04-28 18:04:06 +08:00

386 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::collections::HashMap;
use crate::dto::{EdgeDef, NodeDef, NodeType};
/// 内存中的流程图模型,用于执行引擎。
#[derive(Debug, Clone)]
pub struct FlowGraph {
/// node_id → FlowNode
pub nodes: HashMap<String, FlowNode>,
/// edge_id → FlowEdge
pub edges: HashMap<String, FlowEdge>,
/// node_id → 从该节点出发的边列表
pub outgoing: HashMap<String, Vec<String>>,
/// node_id → 到达该节点的边列表
pub incoming: HashMap<String, Vec<String>>,
/// StartEvent 的 node_id
pub start_node_id: Option<String>,
/// 所有 EndEvent 的 node_id
pub end_node_ids: Vec<String>,
}
/// 内存中的节点模型。
#[derive(Debug, Clone)]
pub struct FlowNode {
pub id: String,
pub node_type: NodeType,
pub name: String,
pub assignee_id: Option<uuid::Uuid>,
pub candidate_groups: Option<Vec<String>>,
pub service_type: Option<String>,
pub service_config: Option<crate::dto::ServiceTaskConfig>,
}
/// 内存中的边模型。
#[derive(Debug, Clone)]
pub struct FlowEdge {
pub id: String,
pub source: String,
pub target: String,
pub condition: Option<String>,
pub label: Option<String>,
}
impl FlowGraph {
/// 从 DTO 节点和边列表构建 FlowGraph。
pub fn build(nodes: &[NodeDef], edges: &[EdgeDef]) -> Self {
let mut graph = FlowGraph {
nodes: HashMap::new(),
edges: HashMap::new(),
outgoing: HashMap::new(),
incoming: HashMap::new(),
start_node_id: None,
end_node_ids: Vec::new(),
};
for n in nodes {
let flow_node = FlowNode {
id: n.id.clone(),
node_type: n.node_type.clone(),
name: n.name.clone(),
assignee_id: n.assignee_id,
candidate_groups: n.candidate_groups.clone(),
service_type: n.service_type.clone(),
service_config: n.service_config.clone(),
};
if n.node_type == NodeType::StartEvent {
graph.start_node_id = Some(n.id.clone());
}
if n.node_type == NodeType::EndEvent {
graph.end_node_ids.push(n.id.clone());
}
graph.nodes.insert(n.id.clone(), flow_node);
graph.outgoing.insert(n.id.clone(), Vec::new());
graph.incoming.insert(n.id.clone(), Vec::new());
}
for e in edges {
graph.edges.insert(
e.id.clone(),
FlowEdge {
id: e.id.clone(),
source: e.source.clone(),
target: e.target.clone(),
condition: e.condition.clone(),
label: e.label.clone(),
},
);
if let Some(out) = graph.outgoing.get_mut(&e.source) {
out.push(e.id.clone());
}
if let Some(inc) = graph.incoming.get_mut(&e.target) {
inc.push(e.id.clone());
}
}
graph
}
/// 获取节点的出边。
pub fn get_outgoing_edges(&self, node_id: &str) -> Vec<&FlowEdge> {
self.outgoing
.get(node_id)
.map(|edge_ids| {
edge_ids
.iter()
.filter_map(|eid| self.edges.get(eid))
.collect()
})
.unwrap_or_default()
}
/// 获取节点的入边。
pub fn get_incoming_edges(&self, node_id: &str) -> Vec<&FlowEdge> {
self.incoming
.get(node_id)
.map(|edge_ids| {
edge_ids
.iter()
.filter_map(|eid| self.edges.get(eid))
.collect()
})
.unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dto::{EdgeDef, NodeDef, NodeType};
fn make_node(id: &str, node_type: NodeType) -> NodeDef {
NodeDef {
id: id.to_string(),
node_type,
name: id.to_string(),
assignee_id: None,
candidate_groups: None,
service_type: None,
service_config: None,
position: None,
}
}
fn make_edge(id: &str, source: &str, target: &str) -> EdgeDef {
EdgeDef {
id: id.to_string(),
source: source.to_string(),
target: target.to_string(),
condition: None,
label: None,
}
}
// ---- build ----
#[test]
fn build_empty_graph() {
let graph = FlowGraph::build(&[], &[]);
assert!(graph.nodes.is_empty());
assert!(graph.edges.is_empty());
assert!(graph.start_node_id.is_none());
assert!(graph.end_node_ids.is_empty());
}
#[test]
fn build_identifies_start_and_end() {
let nodes = vec![
make_node("start", NodeType::StartEvent),
make_node("task1", NodeType::UserTask),
make_node("end", NodeType::EndEvent),
];
let edges = vec![
make_edge("e1", "start", "task1"),
make_edge("e2", "task1", "end"),
];
let graph = FlowGraph::build(&nodes, &edges);
assert_eq!(graph.start_node_id, Some("start".to_string()));
assert_eq!(graph.end_node_ids, vec!["end".to_string()]);
assert_eq!(graph.nodes.len(), 3);
assert_eq!(graph.edges.len(), 2);
}
#[test]
fn build_multiple_end_events() {
let nodes = vec![
make_node("start", NodeType::StartEvent),
make_node("gw", NodeType::ExclusiveGateway),
make_node("end1", NodeType::EndEvent),
make_node("end2", NodeType::EndEvent),
];
let edges = vec![
make_edge("e1", "start", "gw"),
make_edge("e2", "gw", "end1"),
make_edge("e3", "gw", "end2"),
];
let graph = FlowGraph::build(&nodes, &edges);
assert_eq!(graph.end_node_ids.len(), 2);
assert!(graph.end_node_ids.contains(&"end1".to_string()));
assert!(graph.end_node_ids.contains(&"end2".to_string()));
}
#[test]
fn build_copies_node_properties() {
let user_id = {
let ts = uuid::Timestamp::now(uuid::NoContext);
uuid::Uuid::new_v7(ts)
};
let nodes = vec![NodeDef {
id: "task".to_string(),
node_type: NodeType::UserTask,
name: "审批".to_string(),
assignee_id: Some(user_id),
candidate_groups: Some(vec!["managers".to_string()]),
service_type: None,
service_config: None,
position: None,
}];
let graph = FlowGraph::build(&nodes, &[]);
let node = graph.nodes.get("task").unwrap();
assert_eq!(node.name, "审批");
assert_eq!(node.assignee_id, Some(user_id));
assert_eq!(node.candidate_groups, Some(vec!["managers".to_string()]));
}
#[test]
fn build_edge_with_condition_and_label() {
let nodes = vec![
make_node("start", NodeType::StartEvent),
make_node("end", NodeType::EndEvent),
];
let edges = vec![EdgeDef {
id: "e1".to_string(),
source: "start".to_string(),
target: "end".to_string(),
condition: Some("amount > 1000".to_string()),
label: Some("高额审批".to_string()),
}];
let graph = FlowGraph::build(&nodes, &edges);
let edge = graph.edges.get("e1").unwrap();
assert_eq!(edge.condition, Some("amount > 1000".to_string()));
assert_eq!(edge.label, Some("高额审批".to_string()));
}
#[test]
fn build_edge_to_unknown_node_still_recorded() {
let nodes = vec![make_node("start", NodeType::StartEvent)];
// edge target "missing" 不在 nodes 中,但 edge 仍被记录
let edges = vec![make_edge("e1", "start", "missing")];
let graph = FlowGraph::build(&nodes, &edges);
assert_eq!(graph.edges.len(), 1);
// outgoing 为 start 有 e1但 incoming["missing"] 不存在
assert_eq!(graph.outgoing.get("start").unwrap().len(), 1);
assert!(graph.incoming.get("missing").is_none());
}
// ---- get_outgoing_edges ----
#[test]
fn outgoing_edges_for_known_node() {
let nodes = vec![
make_node("start", NodeType::StartEvent),
make_node("end", NodeType::EndEvent),
];
let edges = vec![make_edge("e1", "start", "end")];
let graph = FlowGraph::build(&nodes, &edges);
let out = graph.get_outgoing_edges("start");
assert_eq!(out.len(), 1);
assert_eq!(out[0].target, "end");
}
#[test]
fn outgoing_edges_for_unknown_node_empty() {
let graph = FlowGraph::build(&[], &[]);
assert!(graph.get_outgoing_edges("nonexistent").is_empty());
}
#[test]
fn outgoing_edges_parallel_gateway_fan_out() {
let nodes = vec![
make_node("start", NodeType::StartEvent),
make_node("fork", NodeType::ParallelGateway),
make_node("a", NodeType::UserTask),
make_node("b", NodeType::ServiceTask),
make_node("end", NodeType::EndEvent),
];
let edges = vec![
make_edge("e1", "start", "fork"),
make_edge("e2", "fork", "a"),
make_edge("e3", "fork", "b"),
];
let graph = FlowGraph::build(&nodes, &edges);
let out = graph.get_outgoing_edges("fork");
assert_eq!(out.len(), 2);
let targets: Vec<&str> = out.iter().map(|e| e.target.as_str()).collect();
assert!(targets.contains(&"a"));
assert!(targets.contains(&"b"));
}
// ---- get_incoming_edges ----
#[test]
fn incoming_edges_for_known_node() {
let nodes = vec![
make_node("start", NodeType::StartEvent),
make_node("end", NodeType::EndEvent),
];
let edges = vec![make_edge("e1", "start", "end")];
let graph = FlowGraph::build(&nodes, &edges);
let inc = graph.get_incoming_edges("end");
assert_eq!(inc.len(), 1);
assert_eq!(inc[0].source, "start");
}
#[test]
fn incoming_edges_for_unknown_node_empty() {
let graph = FlowGraph::build(&[], &[]);
assert!(graph.get_incoming_edges("nonexistent").is_empty());
}
#[test]
fn incoming_edges_join_gateway() {
let nodes = vec![
make_node("start", NodeType::StartEvent),
make_node("a", NodeType::UserTask),
make_node("b", NodeType::ServiceTask),
make_node("join", NodeType::ParallelGateway),
make_node("end", NodeType::EndEvent),
];
let edges = vec![
make_edge("e1", "start", "a"),
make_edge("e2", "start", "b"),
make_edge("e3", "a", "join"),
make_edge("e4", "b", "join"),
make_edge("e5", "join", "end"),
];
let graph = FlowGraph::build(&nodes, &edges);
let inc = graph.get_incoming_edges("join");
assert_eq!(inc.len(), 2);
let sources: Vec<&str> = inc.iter().map(|e| e.source.as_str()).collect();
assert!(sources.contains(&"a"));
assert!(sources.contains(&"b"));
}
// ---- start/end 节点无入/出边 ----
#[test]
fn start_node_has_no_incoming() {
let nodes = vec![
make_node("start", NodeType::StartEvent),
make_node("end", NodeType::EndEvent),
];
let edges = vec![make_edge("e1", "start", "end")];
let graph = FlowGraph::build(&nodes, &edges);
assert!(graph.get_incoming_edges("start").is_empty());
assert_eq!(graph.get_outgoing_edges("start").len(), 1);
}
#[test]
fn end_node_has_no_outgoing() {
let nodes = vec![
make_node("start", NodeType::StartEvent),
make_node("end", NodeType::EndEvent),
];
let edges = vec![make_edge("e1", "start", "end")];
let graph = FlowGraph::build(&nodes, &edges);
assert!(graph.get_outgoing_edges("end").is_empty());
assert_eq!(graph.get_incoming_edges("end").len(), 1);
}
}